@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
package/dist/index.js CHANGED
@@ -46,6 +46,27 @@ var __export = (target, all) => {
46
46
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
47
47
  var __require = import.meta.require;
48
48
 
49
+ // src/generated/storage-kit/mode.ts
50
+ function normalizeStorageMode(value) {
51
+ const normalized = value.trim().toLowerCase().replace(/-/g, "_");
52
+ if (normalized === "local")
53
+ return { mode: "local", deprecatedAlias: null };
54
+ if (normalized === "cloud")
55
+ return { mode: "cloud", deprecatedAlias: null };
56
+ if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
57
+ return { mode: "cloud", deprecatedAlias: normalized };
58
+ }
59
+ throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
60
+ }
61
+ var DEPRECATED_STORAGE_MODE_ALIASES;
62
+ var init_mode = __esm(() => {
63
+ DEPRECATED_STORAGE_MODE_ALIASES = [
64
+ "remote",
65
+ "hybrid",
66
+ "self_hosted"
67
+ ];
68
+ });
69
+
49
70
  // src/storage.ts
50
71
  import { Database } from "bun:sqlite";
51
72
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -57,6 +78,12 @@ import pg from "pg";
57
78
  function markServerContext() {
58
79
  _serverContext = true;
59
80
  }
81
+ function resetServerContextForTests() {
82
+ if (process.env["NODE_ENV"] !== "test") {
83
+ throw new Error("resetServerContextForTests is only available under NODE_ENV=test");
84
+ }
85
+ _serverContext = false;
86
+ }
60
87
  function isServerContext() {
61
88
  return _serverContext;
62
89
  }
@@ -307,18 +334,20 @@ function warnDeprecatedStorageMode(alias) {
307
334
  warnedDeprecatedModes.add(alias);
308
335
  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" });
309
336
  }
310
- function normalizeStorageMode(value) {
311
- if (!value)
337
+ function normalizeStorageMode2(value, source) {
338
+ if (!value || !value.trim())
312
339
  return null;
313
- const normalized = value.trim().toLowerCase();
314
- if (normalized === "local" || normalized === "cloud") {
315
- return normalized;
340
+ let normalized;
341
+ try {
342
+ normalized = normalizeStorageMode(value);
343
+ } catch (error) {
344
+ const detail = error instanceof Error ? error.message : String(error);
345
+ throw new Error(`mementos: ${source}=${value} is not a valid mode. ${detail}`);
316
346
  }
317
- if (normalized === "remote" || normalized === "hybrid") {
318
- warnDeprecatedStorageMode(normalized);
319
- return "cloud";
347
+ if (normalized.deprecatedAlias) {
348
+ warnDeprecatedStorageMode(normalized.deprecatedAlias);
320
349
  }
321
- return null;
350
+ return normalized.mode;
322
351
  }
323
352
  function readConfigFile() {
324
353
  if (!existsSync(STORAGE_CONFIG_PATH)) {
@@ -357,7 +386,7 @@ function getStorageDatabaseEnvName() {
357
386
  }
358
387
  function getStorageModeOverride() {
359
388
  for (const env of MODE_ENV_NAMES) {
360
- const value = normalizeStorageMode(readEnv(env.name) ?? undefined);
389
+ const value = normalizeStorageMode2(readEnv(env.name) ?? undefined, env.name);
361
390
  if (value)
362
391
  return value;
363
392
  }
@@ -367,7 +396,7 @@ function getStorageConfig() {
367
396
  const fileConfig = readConfigFile();
368
397
  const modeOverride = getStorageModeOverride();
369
398
  const envConnectionString = getConfiguredConnectionString();
370
- const fileMode = normalizeStorageMode(fileConfig.mode);
399
+ const fileMode = normalizeStorageMode2(fileConfig.mode, `${STORAGE_CONFIG_PATH} "mode"`);
371
400
  const merged = {
372
401
  ...DEFAULT_STORAGE_CONFIG,
373
402
  ...fileConfig,
@@ -794,6 +823,7 @@ CREATE TABLE IF NOT EXISTS _sync_meta (
794
823
  direction TEXT DEFAULT 'push'
795
824
  )`;
796
825
  var init_storage = __esm(() => {
826
+ init_mode();
797
827
  PgSyncPool = class PgSyncPool {
798
828
  worker;
799
829
  status;
@@ -938,7 +968,7 @@ import { tmpdir } from "os";
938
968
  import { join as join2 } from "path";
939
969
  import { writeFileSync as writeFileSync2, unlinkSync } from "fs";
940
970
  import { randomUUID } from "crypto";
941
- function firstEnv(...keys) {
971
+ function firstEnv(keys) {
942
972
  for (const k of keys) {
943
973
  const v = process.env[k]?.trim();
944
974
  if (v)
@@ -946,8 +976,38 @@ function firstEnv(...keys) {
946
976
  }
947
977
  return;
948
978
  }
979
+ function firstEnvKey(keys) {
980
+ for (const k of keys) {
981
+ if (process.env[k]?.trim())
982
+ return k;
983
+ }
984
+ return null;
985
+ }
949
986
  function hasDatabaseUrl() {
950
- return Boolean(firstEnv("HASNA_MEMENTOS_DATABASE_URL", "MEMENTOS_DATABASE_URL"));
987
+ return Boolean(firstEnv(DATABASE_URL_ENV_KEYS));
988
+ }
989
+ function isLoopbackHost(rawHost) {
990
+ const host = rawHost.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
991
+ return host === "localhost" || host === "::1" || /^127\./.test(host);
992
+ }
993
+ function assertRequestAllowedUnderTest(baseUrl) {
994
+ if (process.env["NODE_ENV"] !== "test")
995
+ return;
996
+ if (process.env[ALLOW_REMOTE_API_IN_TESTS_ENV]?.trim())
997
+ return;
998
+ let host;
999
+ try {
1000
+ host = new URL(baseUrl).hostname;
1001
+ } catch {
1002
+ host = "";
1003
+ }
1004
+ if (host && isLoopbackHost(host))
1005
+ return;
1006
+ 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.
1007
+ ` + ` host : ${host || "(unparseable base URL)"}
1008
+ ` + ` 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.
1009
+ ` + " fix : build the child/process env via src/test-support/store-isolation.ts, or point the " + `suite at a loopback stub.
1010
+ ` + ` override : set ${ALLOW_REMOTE_API_IN_TESTS_ENV}=1 only for a test that must reach a remote endpoint.`);
951
1011
  }
952
1012
  function normalizeBase(raw) {
953
1013
  let base = raw.trim().replace(/\/+$/, "");
@@ -955,9 +1015,24 @@ function normalizeBase(raw) {
955
1015
  return base;
956
1016
  return `${base}/v1`;
957
1017
  }
1018
+ function assertUnambiguousStoreEnv() {
1019
+ if (firstEnvKey(DB_PATH_ENV_KEYS))
1020
+ return;
1021
+ if (hasDatabaseUrl())
1022
+ return;
1023
+ const urlKey = firstEnvKey(API_URL_ENV_KEYS);
1024
+ const keyKey = firstEnvKey(API_KEY_ENV_KEYS);
1025
+ if (urlKey && !keyKey) {
1026
+ 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.`);
1027
+ }
1028
+ if (keyKey && !urlKey) {
1029
+ 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.`);
1030
+ }
1031
+ }
958
1032
  function getApiConfig() {
959
- const rawBase = firstEnv("HASNA_MEMENTOS_API_URL", "MEMENTOS_API_URL");
960
- const apiKey = firstEnv("HASNA_MEMENTOS_API_KEY", "MEMENTOS_API_KEY");
1033
+ assertUnambiguousStoreEnv();
1034
+ const rawBase = firstEnv(API_URL_ENV_KEYS);
1035
+ const apiKey = firstEnv(API_KEY_ENV_KEYS);
961
1036
  if (!rawBase || !apiKey)
962
1037
  return null;
963
1038
  return { baseUrl: normalizeBase(rawBase), apiKey };
@@ -971,6 +1046,7 @@ function apiRequestRaw(method, path, body) {
971
1046
  const cfg = getApiConfig();
972
1047
  if (!cfg)
973
1048
  throw new Error("api-mode: not configured (HASNA_MEMENTOS_API_URL / HASNA_MEMENTOS_API_KEY)");
1049
+ assertRequestAllowedUnderTest(cfg.baseUrl);
974
1050
  const url = `${cfg.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
975
1051
  const hasBody = body !== undefined && body !== null;
976
1052
  const timeout = process.env["HASNA_MEMENTOS_API_TIMEOUT"] || DEFAULT_TIMEOUT_S;
@@ -1037,13 +1113,13 @@ x-api-key: ${cfg.apiKey}
1037
1113
  }
1038
1114
  return { status, body: respBody };
1039
1115
  }
1040
- function apiJson(method, path, body) {
1116
+ function apiJson(method, path, body, options) {
1041
1117
  const raw = apiRequestRaw(method, path, body);
1042
1118
  if (raw.status >= 200 && raw.status < 300) {
1043
1119
  const data = raw.body.trim() ? JSON.parse(raw.body) : undefined;
1044
1120
  return { status: raw.status, data };
1045
1121
  }
1046
- if (raw.status === 404) {
1122
+ if (raw.status === 404 && options?.allow404) {
1047
1123
  return { status: 404, data: undefined };
1048
1124
  }
1049
1125
  let msg = `mementos cloud ${method} ${path} \u2192 ${raw.status}`;
@@ -1075,8 +1151,19 @@ function toQuery(params) {
1075
1151
  const s = sp.toString();
1076
1152
  return s ? `?${s}` : "";
1077
1153
  }
1078
- var ApiRequestError, DEFAULT_TIMEOUT_S = "45";
1154
+ 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";
1079
1155
  var init_api_mode = __esm(() => {
1156
+ API_URL_ENV_KEYS = ["HASNA_MEMENTOS_API_URL", "MEMENTOS_API_URL"];
1157
+ API_KEY_ENV_KEYS = ["HASNA_MEMENTOS_API_KEY", "MEMENTOS_API_KEY"];
1158
+ DATABASE_URL_ENV_KEYS = ["HASNA_MEMENTOS_DATABASE_URL", "MEMENTOS_DATABASE_URL"];
1159
+ DB_PATH_ENV_KEYS = ["HASNA_MEMENTOS_DB_PATH", "MEMENTOS_DB_PATH"];
1160
+ MementosStoreConfigError = class MementosStoreConfigError extends Error {
1161
+ code = "MEMENTOS_STORE_CONFIG";
1162
+ constructor(message) {
1163
+ super(message);
1164
+ this.name = "MementosStoreConfigError";
1165
+ }
1166
+ };
1080
1167
  ApiRequestError = class ApiRequestError extends Error {
1081
1168
  status;
1082
1169
  body;
@@ -1090,7 +1177,24 @@ var init_api_mode = __esm(() => {
1090
1177
  });
1091
1178
 
1092
1179
  // src/db/migrations.ts
1093
- var MIGRATIONS;
1180
+ var MEMORY_VERSION_SNAPSHOT_TRIGGER = `
1181
+ CREATE TRIGGER IF NOT EXISTS memories_version_snapshot
1182
+ BEFORE UPDATE ON memories
1183
+ WHEN NEW.version > OLD.version
1184
+ BEGIN
1185
+ INSERT OR IGNORE INTO memory_versions (
1186
+ id, memory_id, version, value, importance, scope, category, tags,
1187
+ summary, pinned, status, when_to_use, created_at
1188
+ ) VALUES (
1189
+ lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-' ||
1190
+ lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(2))) || '-' ||
1191
+ lower(hex(randomblob(6))),
1192
+ OLD.id, OLD.version, OLD.value, OLD.importance, OLD.scope, OLD.category,
1193
+ OLD.tags, OLD.summary, OLD.pinned, OLD.status, OLD.when_to_use,
1194
+ OLD.updated_at
1195
+ );
1196
+ END;
1197
+ `, MIGRATIONS;
1094
1198
  var init_migrations = __esm(() => {
1095
1199
  MIGRATIONS = [
1096
1200
  `
@@ -1988,6 +2092,10 @@ CREATE INDEX IF NOT EXISTS idx_memory_reflection_lessons_memory ON memory_reflec
1988
2092
  CREATE INDEX IF NOT EXISTS idx_memory_reflection_lessons_kind ON memory_reflection_lessons(kind);
1989
2093
 
1990
2094
  INSERT OR IGNORE INTO _migrations (id) VALUES (35);
2095
+ `,
2096
+ `
2097
+ ${MEMORY_VERSION_SNAPSHOT_TRIGGER}
2098
+ INSERT OR IGNORE INTO _migrations (id) VALUES (36);
1991
2099
  `
1992
2100
  ];
1993
2101
  });
@@ -2002,6 +2110,7 @@ __export(exports_database, {
2002
2110
  now: () => now,
2003
2111
  getDbPath: () => getDbPath,
2004
2112
  getDatabase: () => getDatabase,
2113
+ escapeLikePrefix: () => escapeLikePrefix,
2005
2114
  closeDatabase: () => closeDatabase
2006
2115
  });
2007
2116
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, cpSync } from "fs";
@@ -2176,15 +2285,20 @@ function uuid() {
2176
2285
  function shortUuid() {
2177
2286
  return crypto.randomUUID().slice(0, 8);
2178
2287
  }
2288
+ function escapeLikePrefix(s) {
2289
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
2290
+ }
2179
2291
  function resolvePartialId(db, table, partialId) {
2180
2292
  if (!ALLOWED_TABLES.has(table)) {
2181
2293
  throw new Error(`Invalid table name: ${table}`);
2182
2294
  }
2295
+ if (partialId === "")
2296
+ return null;
2183
2297
  if (partialId.length >= 36) {
2184
2298
  const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
2185
2299
  return row?.id ?? null;
2186
2300
  }
2187
- const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${partialId}%`);
2301
+ const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ? ESCAPE '\\'`).all(`${escapeLikePrefix(partialId)}%`);
2188
2302
  if (rows.length === 1) {
2189
2303
  return rows[0].id;
2190
2304
  }
@@ -49293,7 +49407,10 @@ function parseMemoryRow(row) {
49293
49407
  }
49294
49408
  function createMemory(input, dedupeMode = "merge", db) {
49295
49409
  if (!db && isApiMode()) {
49296
- const { data } = apiJson("POST", "/memories", { ...input, dedupe: dedupeMode });
49410
+ const { status, data } = apiJson("POST", "/memories", { ...input, dedupe: dedupeMode });
49411
+ if (!data || !data.id) {
49412
+ throw new ApiRequestError(`mementos cloud POST /memories \u2192 ${status} but no memory was returned; the write did not persist (key: ${input.key})`, status, "");
49413
+ }
49297
49414
  return data;
49298
49415
  }
49299
49416
  const d = db || getDatabase();
@@ -49450,7 +49567,7 @@ function listMemoriesByKey(key, db) {
49450
49567
  }
49451
49568
  function getMemory(id, db) {
49452
49569
  if (!db && isApiMode()) {
49453
- const { status, data } = apiJson("GET", `/memories/${encodeURIComponent(id)}`);
49570
+ const { status, data } = apiJson("GET", `/memories/${encodeURIComponent(id)}`, undefined, { allow404: true });
49454
49571
  return status === 404 ? null : data ?? null;
49455
49572
  }
49456
49573
  const d = db || getDatabase();
@@ -49674,36 +49791,22 @@ function listMemories(filter, db) {
49674
49791
  }
49675
49792
  function updateMemory(id, input, db) {
49676
49793
  if (!db && isApiMode()) {
49677
- const { status, data } = apiJson("PATCH", `/memories/${encodeURIComponent(id)}`, input);
49794
+ const { status, data } = apiJson("PATCH", `/memories/${encodeURIComponent(id)}`, input, { allow404: true });
49678
49795
  if (status === 404)
49679
49796
  throw new MemoryNotFoundError(id);
49797
+ if (data && typeof input.version === "number" && typeof data.version === "number" && data.version <= input.version) {
49798
+ 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.`);
49799
+ }
49680
49800
  return data;
49681
49801
  }
49682
49802
  const d = db || getDatabase();
49683
49803
  const existing = getMemory(id, d);
49684
49804
  if (!existing)
49685
49805
  throw new MemoryNotFoundError(id);
49806
+ const memoryId = existing.id;
49686
49807
  if (existing.version !== input.version) {
49687
49808
  throw new VersionConflictError(id, input.version, existing.version);
49688
49809
  }
49689
- try {
49690
- 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)
49691
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
49692
- uuid(),
49693
- existing.id,
49694
- existing.version,
49695
- existing.value,
49696
- existing.importance,
49697
- existing.scope,
49698
- existing.category,
49699
- JSON.stringify(existing.tags),
49700
- existing.summary,
49701
- existing.pinned ? 1 : 0,
49702
- existing.status,
49703
- existing.when_to_use || null,
49704
- existing.updated_at
49705
- ]);
49706
- } catch {}
49707
49810
  const sets = ["version = version + 1", "updated_at = ?"];
49708
49811
  const params = [now()];
49709
49812
  if (input.value !== undefined) {
@@ -49753,15 +49856,18 @@ function updateMemory(id, input, db) {
49753
49856
  if (input.tags !== undefined) {
49754
49857
  sets.push("tags = ?");
49755
49858
  params.push(JSON.stringify(input.tags));
49756
- d.run("DELETE FROM memory_tags WHERE memory_id = ?", [id]);
49859
+ d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memoryId]);
49757
49860
  const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
49758
49861
  for (const tag of input.tags) {
49759
- insertTag.run(id, tag);
49862
+ insertTag.run(memoryId, tag);
49760
49863
  }
49761
49864
  }
49762
- params.push(id);
49763
- d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
49764
- const updated = getMemory(id, d);
49865
+ params.push(memoryId);
49866
+ const result = d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
49867
+ if (result.changes === 0) {
49868
+ 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.`);
49869
+ }
49870
+ const updated = getMemory(memoryId, d);
49765
49871
  if (input.value !== undefined) {
49766
49872
  try {
49767
49873
  const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
@@ -49782,14 +49888,15 @@ function updateMemory(id, input, db) {
49782
49888
  }
49783
49889
  function deleteMemory(id, db) {
49784
49890
  if (!db && isApiMode()) {
49785
- const { status } = apiJson("DELETE", `/memories/${encodeURIComponent(id)}`);
49891
+ const { status } = apiJson("DELETE", `/memories/${encodeURIComponent(id)}`, undefined, { allow404: true });
49786
49892
  return status !== 404;
49787
49893
  }
49788
49894
  const d = db || getDatabase();
49789
- const result = d.run("DELETE FROM memories WHERE id = ?", [id]);
49895
+ const memoryId = resolvePartialId(d, "memories", id) ?? id;
49896
+ const result = d.run("DELETE FROM memories WHERE id = ?", [memoryId]);
49790
49897
  if (result.changes > 0) {
49791
49898
  hookRegistry.runHooks("PostMemoryDelete", {
49792
- memoryId: id,
49899
+ memoryId,
49793
49900
  timestamp: Date.now()
49794
49901
  });
49795
49902
  }
@@ -49803,11 +49910,12 @@ function bulkDeleteMemories(ids, db) {
49803
49910
  return data?.deleted ?? 0;
49804
49911
  }
49805
49912
  const d = db || getDatabase();
49806
- const placeholders = ids.map(() => "?").join(",");
49807
- const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...ids);
49913
+ const resolvedIds = ids.map((id) => resolvePartialId(d, "memories", id) ?? id);
49914
+ const placeholders = resolvedIds.map(() => "?").join(",");
49915
+ const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...resolvedIds);
49808
49916
  const count = countRow.c;
49809
49917
  if (count > 0) {
49810
- d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
49918
+ d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, resolvedIds);
49811
49919
  }
49812
49920
  return count;
49813
49921
  }
@@ -49952,7 +50060,7 @@ function registerAgent(name, sessionId, description, role, projectId, db) {
49952
50060
  }
49953
50061
  function getAgent(idOrName, db) {
49954
50062
  if (!db && isApiMode()) {
49955
- const { status, data } = apiJson("GET", `/agents/${encodeURIComponent(idOrName)}`);
50063
+ const { status, data } = apiJson("GET", `/agents/${encodeURIComponent(idOrName)}`, undefined, { allow404: true });
49956
50064
  if (status === 404 || !data)
49957
50065
  return null;
49958
50066
  return data;
@@ -49964,7 +50072,7 @@ function getAgent(idOrName, db) {
49964
50072
  row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(idOrName.trim().toLowerCase());
49965
50073
  if (row)
49966
50074
  return parseAgentRow(row);
49967
- const rows = d.query("SELECT * FROM agents WHERE id LIKE ?").all(`${idOrName}%`);
50075
+ const rows = d.query("SELECT * FROM agents WHERE id LIKE ? ESCAPE '\\'").all(`${escapeLikePrefix(idOrName)}%`);
49968
50076
  if (rows.length === 1)
49969
50077
  return parseAgentRow(rows[0]);
49970
50078
  return null;
@@ -50005,7 +50113,7 @@ function listAgentsByProject(projectId, db) {
50005
50113
  }
50006
50114
  function updateAgent(id, updates, db) {
50007
50115
  if (!db && isApiMode()) {
50008
- const { status, data } = apiJson("PATCH", `/agents/${encodeURIComponent(id)}`, updates);
50116
+ const { status, data } = apiJson("PATCH", `/agents/${encodeURIComponent(id)}`, updates, { allow404: true });
50009
50117
  if (status === 404 || !data)
50010
50118
  return null;
50011
50119
  return data;
@@ -50112,7 +50220,7 @@ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive",
50112
50220
  }
50113
50221
  function releaseLock(lockId, agentId, db) {
50114
50222
  if (!db && isApiMode()) {
50115
- const { status } = apiJson("DELETE", `/locks/${encodeURIComponent(lockId)}`, { agent_id: agentId });
50223
+ const { status } = apiJson("DELETE", `/locks/${encodeURIComponent(lockId)}`, { agent_id: agentId }, { allow404: true });
50116
50224
  return status !== 404;
50117
50225
  }
50118
50226
  const d = db || getDatabase();
@@ -50321,7 +50429,7 @@ function registerProject(name, path, description, memoryPrefix, db) {
50321
50429
  }
50322
50430
  function getProject(idOrPath, db) {
50323
50431
  if (!db && isApiMode()) {
50324
- const { status, data } = apiJson("GET", `/projects/${encodeURIComponent(idOrPath)}`);
50432
+ const { status, data } = apiJson("GET", `/projects/${encodeURIComponent(idOrPath)}`, undefined, { allow404: true });
50325
50433
  if (status === 404 || !data)
50326
50434
  return null;
50327
50435
  return data;
@@ -50514,7 +50622,7 @@ function createEntity(input, db) {
50514
50622
  }
50515
50623
  function getEntity(id, db) {
50516
50624
  if (!db && isApiMode()) {
50517
- const { status, data } = apiJson("GET", `/entities/${encodeURIComponent(id)}`);
50625
+ const { status, data } = apiJson("GET", `/entities/${encodeURIComponent(id)}`, undefined, { allow404: true });
50518
50626
  if (status === 404 || !data)
50519
50627
  throw new EntityNotFoundError(id);
50520
50628
  return data;
@@ -50597,7 +50705,7 @@ function listEntities(filter = {}, db) {
50597
50705
  }
50598
50706
  function updateEntity(id, input, db) {
50599
50707
  if (!db && isApiMode()) {
50600
- const { status, data } = apiJson("PATCH", `/entities/${encodeURIComponent(id)}`, input);
50708
+ const { status, data } = apiJson("PATCH", `/entities/${encodeURIComponent(id)}`, input, { allow404: true });
50601
50709
  if (status === 404 || !data)
50602
50710
  throw new EntityNotFoundError(id);
50603
50711
  return data;
@@ -50630,7 +50738,7 @@ function updateEntity(id, input, db) {
50630
50738
  }
50631
50739
  function deleteEntity(id, db) {
50632
50740
  if (!db && isApiMode()) {
50633
- const { status } = apiJson("DELETE", `/entities/${encodeURIComponent(id)}`);
50741
+ const { status } = apiJson("DELETE", `/entities/${encodeURIComponent(id)}`, undefined, { allow404: true });
50634
50742
  if (status === 404)
50635
50743
  throw new EntityNotFoundError(id);
50636
50744
  return;
@@ -52691,7 +52799,7 @@ function createRelation(input, db) {
52691
52799
  }
52692
52800
  function getRelation(id, db) {
52693
52801
  if (!db && isApiMode()) {
52694
- const { status, data } = apiJson("GET", `/relations/${encodeURIComponent(id)}`);
52802
+ const { status, data } = apiJson("GET", `/relations/${encodeURIComponent(id)}`, undefined, { allow404: true });
52695
52803
  if (status === 404 || !data)
52696
52804
  throw new Error(`Relation not found: ${id}`);
52697
52805
  return data;
@@ -52736,7 +52844,7 @@ function listRelations(filter, db) {
52736
52844
  }
52737
52845
  function deleteRelation(id, db) {
52738
52846
  if (!db && isApiMode()) {
52739
- const { status } = apiJson("DELETE", `/relations/${encodeURIComponent(id)}`);
52847
+ const { status } = apiJson("DELETE", `/relations/${encodeURIComponent(id)}`, undefined, { allow404: true });
52740
52848
  if (status === 404)
52741
52849
  throw new Error(`Relation not found: ${id}`);
52742
52850
  return;
@@ -53379,6 +53487,32 @@ class AutoMemoryQueue {
53379
53487
  getStats() {
53380
53488
  return { ...this.stats, pending: this.queue.length };
53381
53489
  }
53490
+ async waitForIdleForTests(timeoutMs = 3000) {
53491
+ const start = Date.now();
53492
+ while (Date.now() - start < timeoutMs) {
53493
+ if (this.queue.length === 0 && this.activeCount === 0)
53494
+ return;
53495
+ await new Promise((r) => setTimeout(r, 20));
53496
+ }
53497
+ throw new Error("autoMemoryQueue did not become idle before test reset");
53498
+ }
53499
+ resetForTests(handler) {
53500
+ if (this.activeCount !== 0) {
53501
+ throw new Error("Cannot reset autoMemoryQueue while jobs are processing");
53502
+ }
53503
+ this.queue = [];
53504
+ this.running = false;
53505
+ this.stats = {
53506
+ pending: 0,
53507
+ processing: 0,
53508
+ processed: 0,
53509
+ failed: 0,
53510
+ dropped: 0
53511
+ };
53512
+ if (handler !== undefined) {
53513
+ this.handler = handler;
53514
+ }
53515
+ }
53382
53516
  startLoop() {
53383
53517
  this.running = true;
53384
53518
  this.loop();
@@ -36,6 +36,8 @@ declare class AutoMemoryQueue {
36
36
  */
37
37
  enqueue(job: ExtractionJob): void;
38
38
  getStats(): Readonly<QueueStats>;
39
+ waitForIdleForTests(timeoutMs?: number): Promise<void>;
40
+ resetForTests(handler?: JobHandler | null): void;
39
41
  private startLoop;
40
42
  private loop;
41
43
  private processJob;
@@ -1 +1 @@
1
- {"version":3,"file":"auto-memory-queue.d.ts","sourceRoot":"","sources":["../../src/lib/auto-memory-queue.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;CACxC;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,KAAK,UAAU,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAKxD,cAAM,eAAe;IACnB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAK;IAExB,OAAO,CAAC,KAAK,CAMX;IAEF,mDAAmD;IACnD,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAKrC;;;OAGG;IACH,OAAO,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI;IAYjC,QAAQ,IAAI,QAAQ,CAAC,UAAU,CAAC;IAIhC,OAAO,CAAC,SAAS;YAKH,IAAI;YAiBJ,UAAU;CAiBzB;AAED,kDAAkD;AAClD,eAAO,MAAM,eAAe,iBAAwB,CAAC"}
1
+ {"version":3,"file":"auto-memory-queue.d.ts","sourceRoot":"","sources":["../../src/lib/auto-memory-queue.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;CACxC;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,KAAK,UAAU,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAKxD,cAAM,eAAe;IACnB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAK;IAExB,OAAO,CAAC,KAAK,CAMX;IAEF,mDAAmD;IACnD,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAKrC;;;OAGG;IACH,OAAO,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI;IAYjC,QAAQ,IAAI,QAAQ,CAAC,UAAU,CAAC;IAI1B,mBAAmB,CAAC,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAS1D,aAAa,CAAC,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI,GAAG,IAAI;IAkBhD,OAAO,CAAC,SAAS;YAKH,IAAI;YAiBJ,UAAU;CAiBzB;AAED,kDAAkD;AAClD,eAAO,MAAM,eAAe,iBAAwB,CAAC"}
@@ -15,4 +15,5 @@ export declare function processConversationTurn(turn: string, context: Omit<Extr
15
15
  export declare function getAutoMemoryStats(): Readonly<import("./auto-memory-queue.js").QueueStats>;
16
16
  /** Configure the auto-memory pipeline at runtime */
17
17
  export declare function configureAutoMemory(config: Parameters<typeof providerRegistry.configure>[0]): void;
18
+ export declare function resetAutoMemoryForTests(): Promise<void>;
18
19
  //# sourceMappingURL=auto-memory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auto-memory.d.ts","sourceRoot":"","sources":["../../src/lib/auto-memory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAgO7E;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,WAAW,CAAC,EAClD,MAAM,GAAE,aAAa,CAAC,QAAQ,CAAU,GACvC,IAAI,CAQN;AAED,8BAA8B;AAC9B,wBAAgB,kBAAkB,0DAEjC;AAED,oDAAoD;AACpD,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GACvD,IAAI,CAEN"}
1
+ {"version":3,"file":"auto-memory.d.ts","sourceRoot":"","sources":["../../src/lib/auto-memory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAgO7E;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,WAAW,CAAC,EAClD,MAAM,GAAE,aAAa,CAAC,QAAQ,CAAU,GACvC,IAAI,CAQN;AAED,8BAA8B;AAC9B,wBAAgB,kBAAkB,0DAEjC;AAED,oDAAoD;AACpD,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GACvD,IAAI,CAEN;AAED,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC,CAK7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"built-in-hooks.d.ts","sourceRoot":"","sources":["../../src/lib/built-in-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAwOlD,wBAAgB,kBAAkB,IAAI,IAAI,CAyBzC;AAuBD,wBAAgB,cAAc,IAAI,IAAI,CAGrC;AAGD,YAAY,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"built-in-hooks.d.ts","sourceRoot":"","sources":["../../src/lib/built-in-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAyOlD,wBAAgB,kBAAkB,IAAI,IAAI,CAyBzC;AAuBD,wBAAgB,cAAc,IAAI,IAAI,CAGrC;AAGD,YAAY,EAAE,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,20 @@
1
+ export interface EnumViolation {
2
+ field: string;
3
+ value: string;
4
+ allowed: readonly string[];
5
+ }
6
+ /** Human-readable, actionable rejection message naming the field and the accepted set. */
7
+ export declare function formatEnumViolation(v: EnumViolation): string;
8
+ /**
9
+ * Validate one enum-constrained field. Returns the violation, or `null` when
10
+ * the value is acceptable. `undefined`/`null` are acceptable — they mean "leave
11
+ * the column at its default", which is a separate concern from a bad value.
12
+ */
13
+ export declare function validateEnumField(field: string, value: unknown): EnumViolation | null;
14
+ /**
15
+ * Validate every enum-constrained field present on a create/update payload.
16
+ * Returns the first violation so the caller can fail fast with one clear
17
+ * message, or `null` when the payload is clean.
18
+ */
19
+ export declare function validateMemoryEnums(input: Record<string, unknown>): EnumViolation | null;
20
+ //# sourceMappingURL=enum-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enum-validation.d.ts","sourceRoot":"","sources":["../../src/lib/enum-validation.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAMrF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,aAAa,GAAG,IAAI,CAOxF"}
@@ -1 +1 @@
1
- {"version":3,"file":"gdpr.d.ts","sourceRoot":"","sources":["../../src/lib/gdpr.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG1D,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACd,EACN,EAAE,CAAC,EAAE,QAAQ,GACZ,iBAAiB,CAwDnB"}
1
+ {"version":3,"file":"gdpr.d.ts","sourceRoot":"","sources":["../../src/lib/gdpr.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG1D,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACd,EACN,EAAE,CAAC,EAAE,QAAQ,GACZ,iBAAiB,CA+FnB"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4CpE,0FAA0F;AAC1F,eAAO,IAAI,SAAS,EAAE,SAAS,GAAG,IAAW,CAAC;AAqB9C,wBAAgB,WAAW,IAAI,SAAS,CA2EvC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4CpE,0FAA0F;AAC1F,eAAO,IAAI,SAAS,EAAE,SAAS,GAAG,IAAW,CAAC;AAsB9C,wBAAgB,WAAW,IAAI,SAAS,CA2EvC"}