@hasna/mementos 0.15.2 → 0.17.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 (130) hide show
  1. package/README.md +83 -22
  2. package/dist/audit-contract.d.ts +87 -0
  3. package/dist/audit-contract.d.ts.map +1 -0
  4. package/dist/cli/commands/agent.d.ts.map +1 -1
  5. package/dist/cli/commands/consolidation.d.ts.map +1 -1
  6. package/dist/cli/commands/info-context.d.ts.map +1 -1
  7. package/dist/cli/commands/info-history.d.ts.map +1 -1
  8. package/dist/cli/commands/io-backup.d.ts.map +1 -1
  9. package/dist/cli/commands/io-export.d.ts.map +1 -1
  10. package/dist/cli/commands/io-restore.d.ts.map +1 -1
  11. package/dist/cli/commands/memory-cmd-crud.d.ts.map +1 -1
  12. package/dist/cli/commands/memory-cmd-list.d.ts +25 -0
  13. package/dist/cli/commands/memory-cmd-list.d.ts.map +1 -1
  14. package/dist/cli/commands/memory-cmd-recall.d.ts.map +1 -1
  15. package/dist/cli/commands/memory-cmd-remove.d.ts.map +1 -1
  16. package/dist/cli/commands/memory-cmd-search.d.ts.map +1 -1
  17. package/dist/cli/commands/memory-cmd-view.d.ts.map +1 -1
  18. package/dist/cli/commands/memory-cmd-when-to-use.d.ts.map +1 -1
  19. package/dist/cli/commands/project.d.ts.map +1 -1
  20. package/dist/cli/commands/system-session.d.ts.map +1 -1
  21. package/dist/cli/commands/system-status.d.ts.map +1 -1
  22. package/dist/cli/commands/system-synthesized-profile.d.ts.map +1 -1
  23. package/dist/cli/helpers.d.ts +3 -1
  24. package/dist/cli/helpers.d.ts.map +1 -1
  25. package/dist/cli/index.js +2482 -823
  26. package/dist/cli/structured-json.d.ts +54 -0
  27. package/dist/cli/structured-json.d.ts.map +1 -0
  28. package/dist/db/api-mode.d.ts +10 -0
  29. package/dist/db/api-mode.d.ts.map +1 -1
  30. package/dist/db/api-response-contract.d.ts +23 -0
  31. package/dist/db/api-response-contract.d.ts.map +1 -0
  32. package/dist/db/audit.d.ts +34 -45
  33. package/dist/db/audit.d.ts.map +1 -1
  34. package/dist/db/database.d.ts +1 -0
  35. package/dist/db/database.d.ts.map +1 -1
  36. package/dist/db/locks.d.ts.map +1 -1
  37. package/dist/db/machines.d.ts +50 -13
  38. package/dist/db/machines.d.ts.map +1 -1
  39. package/dist/db/memories.d.ts.map +1 -1
  40. package/dist/db/migrations.d.ts.map +1 -1
  41. package/dist/db/pg-migrate.d.ts.map +1 -1
  42. package/dist/db/pg-migrations.d.ts.map +1 -1
  43. package/dist/db/session-jobs.d.ts +2 -0
  44. package/dist/db/session-jobs.d.ts.map +1 -1
  45. package/dist/diagnostics/historical-project-registration-receipt.js +215 -1
  46. package/dist/index.d.ts +4 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +1523 -206
  49. package/dist/lib/built-in-hooks.d.ts.map +1 -1
  50. package/dist/lib/conversations-transport.d.ts +21 -0
  51. package/dist/lib/conversations-transport.d.ts.map +1 -0
  52. package/dist/lib/export-v1.d.ts +2 -0
  53. package/dist/lib/export-v1.d.ts.map +1 -1
  54. package/dist/lib/injection-project.d.ts +10 -0
  55. package/dist/lib/injection-project.d.ts.map +1 -0
  56. package/dist/lib/injector.d.ts +1 -0
  57. package/dist/lib/injector.d.ts.map +1 -1
  58. package/dist/lib/machine-visibility.d.ts.map +1 -1
  59. package/dist/lib/memory-lock.d.ts.map +1 -1
  60. package/dist/lib/profile-synthesizer.d.ts +13 -2
  61. package/dist/lib/profile-synthesizer.d.ts.map +1 -1
  62. package/dist/lib/session-queue.d.ts.map +1 -1
  63. package/dist/lib/session-registry.d.ts +38 -1
  64. package/dist/lib/session-registry.d.ts.map +1 -1
  65. package/dist/lib/storage-sync.d.ts +5 -0
  66. package/dist/lib/storage-sync.d.ts.map +1 -1
  67. package/dist/lib/synthesis/index.d.ts.map +1 -1
  68. package/dist/mcp/index.d.ts +1 -1
  69. package/dist/mcp/index.d.ts.map +1 -1
  70. package/dist/mcp/index.js +3857 -1600
  71. package/dist/mcp/memory-broadcast.d.ts +6 -8
  72. package/dist/mcp/memory-broadcast.d.ts.map +1 -1
  73. package/dist/mcp/profile.d.ts +23 -0
  74. package/dist/mcp/profile.d.ts.map +1 -0
  75. package/dist/mcp/tools/bounded-output.d.ts +27 -0
  76. package/dist/mcp/tools/bounded-output.d.ts.map +1 -0
  77. package/dist/mcp/tools/lock-tools.d.ts.map +1 -1
  78. package/dist/mcp/tools/memory-audit.d.ts +1 -1
  79. package/dist/mcp/tools/memory-audit.d.ts.map +1 -1
  80. package/dist/mcp/tools/memory-crud.d.ts.map +1 -1
  81. package/dist/mcp/tools/memory-inject.d.ts.map +1 -1
  82. package/dist/mcp/tools/memory-io.d.ts.map +1 -1
  83. package/dist/mcp/tools/project-tools.d.ts.map +1 -1
  84. package/dist/mcp/tools/session-tools.d.ts.map +1 -1
  85. package/dist/mcp/tools/tool-registry.d.ts +22 -19
  86. package/dist/mcp/tools/tool-registry.d.ts.map +1 -1
  87. package/dist/mcp/tools/utility-tools.d.ts +2 -1
  88. package/dist/mcp/tools/utility-tools.d.ts.map +1 -1
  89. package/dist/project-registration.js +276 -20
  90. package/dist/sdk/index.d.ts +110 -7
  91. package/dist/sdk/index.d.ts.map +1 -1
  92. package/dist/sdk/index.js +746 -28
  93. package/dist/server/index.d.ts +2 -0
  94. package/dist/server/index.d.ts.map +1 -1
  95. package/dist/server/index.js +3195 -1134
  96. package/dist/server/openapi.d.ts.map +1 -1
  97. package/dist/server/routes/audit.d.ts +2 -0
  98. package/dist/server/routes/audit.d.ts.map +1 -0
  99. package/dist/server/routes/machines.d.ts +2 -0
  100. package/dist/server/routes/machines.d.ts.map +1 -0
  101. package/dist/server/routes/system-sessions.d.ts.map +1 -1
  102. package/dist/server/routes/system-synthesis.d.ts.map +1 -1
  103. package/dist/types/index.d.ts +3 -0
  104. package/dist/types/index.d.ts.map +1 -1
  105. package/hasna.contract.json +2 -2
  106. package/package.json +8 -5
  107. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts +0 -2
  108. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts.map +0 -1
  109. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts +0 -2
  110. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts.map +0 -1
  111. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts +0 -2
  112. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts.map +0 -1
  113. package/dist/db/__fixtures__/list-filter-capture-server.d.ts +0 -2
  114. package/dist/db/__fixtures__/list-filter-capture-server.d.ts.map +0 -1
  115. package/dist/db/__fixtures__/list-filter-client-runner.d.ts +0 -2
  116. package/dist/db/__fixtures__/list-filter-client-runner.d.ts.map +0 -1
  117. package/dist/test-support/memories-page-stub-server.d.ts +0 -2
  118. package/dist/test-support/memories-page-stub-server.d.ts.map +0 -1
  119. package/dist/test-support/memories-page-stub.d.ts +0 -28
  120. package/dist/test-support/memories-page-stub.d.ts.map +0 -1
  121. package/dist/test-support/pg-sync-stub-worker.d.ts +0 -2
  122. package/dist/test-support/pg-sync-stub-worker.d.ts.map +0 -1
  123. package/dist/test-support/preload-local-store.d.ts +0 -2
  124. package/dist/test-support/preload-local-store.d.ts.map +0 -1
  125. package/dist/test-support/project-authority-identity.d.ts +0 -8
  126. package/dist/test-support/project-authority-identity.d.ts.map +0 -1
  127. package/dist/test-support/store-isolation.d.ts +0 -147
  128. package/dist/test-support/store-isolation.d.ts.map +0 -1
  129. package/dist/test-support/strip-ansi.d.ts +0 -3
  130. package/dist/test-support/strip-ansi.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1239,6 +1239,8 @@ function getApiConfig(env2 = process.env, options = {}) {
1239
1239
  function isApiMode() {
1240
1240
  if (hasDatabaseUrl())
1241
1241
  return false;
1242
+ if (isServerContext())
1243
+ return false;
1242
1244
  return getApiConfig() !== null;
1243
1245
  }
1244
1246
  function assertClientStoreConfigured(env2 = process.env, options = {}) {
@@ -1285,7 +1287,14 @@ x-api-key: ${cfg.apiKey}
1285
1287
  let bodyFile;
1286
1288
  if (hasBody) {
1287
1289
  bodyFile = join3(tmpdir(), `mem-req-${process.pid}-${randomUUID()}.json`);
1288
- writeFileSync2(bodyFile, JSON.stringify(body), { mode: 384 });
1290
+ try {
1291
+ writeFileSync2(bodyFile, JSON.stringify(body), { mode: 384 });
1292
+ } catch (error) {
1293
+ try {
1294
+ unlinkSync(bodyFile);
1295
+ } catch {}
1296
+ throw error;
1297
+ }
1289
1298
  args.push("--data-binary", `@${bodyFile}`);
1290
1299
  }
1291
1300
  args.push(url);
@@ -1333,7 +1342,7 @@ function apiJson(method, path, body, options) {
1333
1342
  try {
1334
1343
  data = JSON.parse(raw.body);
1335
1344
  } catch (e) {
1336
- throw new ApiRequestError(`mementos cloud ${method} ${path} returned status ${raw.status} with a body that is not valid JSON (${e instanceof Error ? e.message : String(e)}) \u2014 the response is truncated or the server is unhealthy`, raw.status, raw.body.slice(0, 500));
1345
+ throw new ApiRequestError(`mementos cloud ${method} ${path} returned status ${raw.status} with a body that is not valid JSON (${e instanceof Error ? e.message : String(e)}) \u2014 the response is truncated or the server is unhealthy`, raw.status, "");
1337
1346
  }
1338
1347
  } else {
1339
1348
  data = undefined;
@@ -1375,6 +1384,7 @@ function toQuery(params) {
1375
1384
  var API_URL_ENV_KEYS, API_KEY_ENV_KEYS, DATABASE_URL_ENV_KEYS, DB_PATH_ENV_KEYS, ALLOW_REMOTE_API_IN_TESTS_ENV = "MEMENTOS_ALLOW_REMOTE_API_IN_TESTS", MementosStoreConfigError, ApiRequestError, DEFAULT_TIMEOUT_S = "45";
1376
1385
  var init_api_mode = __esm(() => {
1377
1386
  init_local_opt_in();
1387
+ init_storage();
1378
1388
  API_URL_ENV_KEYS = clientTransportEnvKeys2("mementos").apiUrlKeys;
1379
1389
  API_KEY_ENV_KEYS = clientTransportEnvKeys2("mementos").apiKeyKeys;
1380
1390
  DATABASE_URL_ENV_KEYS = ["HASNA_MEMENTOS_DATABASE_URL", "MEMENTOS_DATABASE_URL"];
@@ -2787,6 +2797,220 @@ INSERT OR IGNORE INTO _migrations (id) VALUES (38);
2787
2797
  `
2788
2798
  ${sqliteMementosMemoryProjectLinkSchemaSql()}
2789
2799
  INSERT OR IGNORE INTO _migrations (id) VALUES (40);
2800
+ `,
2801
+ `
2802
+ DROP TABLE IF EXISTS temp.mementos_m41_machine_preflight;
2803
+ CREATE TEMP TABLE mementos_m41_machine_preflight (
2804
+ id TEXT PRIMARY KEY,
2805
+ name TEXT NOT NULL CHECK(
2806
+ length(name) BETWEEN 1 AND 128
2807
+ AND instr(name, char(0)) = 0
2808
+ AND instr(name, char(1)) = 0
2809
+ AND instr(name, char(2)) = 0
2810
+ AND instr(name, char(3)) = 0
2811
+ AND instr(name, char(4)) = 0
2812
+ AND instr(name, char(5)) = 0
2813
+ AND instr(name, char(6)) = 0
2814
+ AND instr(name, char(7)) = 0
2815
+ AND instr(name, char(8)) = 0
2816
+ AND instr(name, char(9)) = 0
2817
+ AND instr(name, char(10)) = 0
2818
+ AND instr(name, char(11)) = 0
2819
+ AND instr(name, char(12)) = 0
2820
+ AND instr(name, char(13)) = 0
2821
+ AND instr(name, char(14)) = 0
2822
+ AND instr(name, char(15)) = 0
2823
+ AND instr(name, char(16)) = 0
2824
+ AND instr(name, char(17)) = 0
2825
+ AND instr(name, char(18)) = 0
2826
+ AND instr(name, char(19)) = 0
2827
+ AND instr(name, char(20)) = 0
2828
+ AND instr(name, char(21)) = 0
2829
+ AND instr(name, char(22)) = 0
2830
+ AND instr(name, char(23)) = 0
2831
+ AND instr(name, char(24)) = 0
2832
+ AND instr(name, char(25)) = 0
2833
+ AND instr(name, char(26)) = 0
2834
+ AND instr(name, char(27)) = 0
2835
+ AND instr(name, char(28)) = 0
2836
+ AND instr(name, char(29)) = 0
2837
+ AND instr(name, char(30)) = 0
2838
+ AND instr(name, char(31)) = 0
2839
+ AND instr(name, char(127)) = 0
2840
+ ),
2841
+ hostname TEXT NOT NULL UNIQUE CHECK(
2842
+ length(hostname) BETWEEN 1 AND 253
2843
+ AND hostname = lower(rtrim(trim(hostname), '.'))
2844
+ AND instr(hostname, ' ') = 0
2845
+ AND instr(hostname, char(9)) = 0
2846
+ AND instr(hostname, char(10)) = 0
2847
+ AND instr(hostname, char(13)) = 0
2848
+ AND instr(hostname, '/') = 0
2849
+ AND instr(hostname, char(92)) = 0
2850
+ ),
2851
+ platform TEXT NOT NULL CHECK(
2852
+ length(platform) BETWEEN 1 AND 64
2853
+ AND platform = lower(trim(platform))
2854
+ AND platform NOT GLOB '*[^a-z0-9._-]*'
2855
+ ),
2856
+ is_primary INTEGER NOT NULL CHECK(is_primary IN (0, 1)),
2857
+ created_at TEXT NOT NULL CHECK(julianday(created_at) IS NOT NULL),
2858
+ last_seen_at TEXT NOT NULL CHECK(
2859
+ julianday(last_seen_at) IS NOT NULL
2860
+ AND julianday(last_seen_at) >= julianday(created_at)
2861
+ )
2862
+ );
2863
+ INSERT INTO mementos_m41_machine_preflight
2864
+ (id, name, hostname, platform, is_primary, created_at, last_seen_at)
2865
+ SELECT
2866
+ id,
2867
+ name,
2868
+ lower(rtrim(trim(hostname), '.')),
2869
+ platform,
2870
+ is_primary,
2871
+ created_at,
2872
+ last_seen_at
2873
+ FROM machines;
2874
+
2875
+ UPDATE machines
2876
+ SET hostname = (
2877
+ SELECT preflight.hostname
2878
+ FROM mementos_m41_machine_preflight preflight
2879
+ WHERE preflight.id = machines.id
2880
+ );
2881
+
2882
+ DROP TABLE mementos_m41_machine_preflight;
2883
+ DROP TRIGGER IF EXISTS machines_single_primary_insert;
2884
+ DROP TRIGGER IF EXISTS machines_single_primary_update;
2885
+ DROP INDEX IF EXISTS idx_machines_hostname;
2886
+ CREATE UNIQUE INDEX idx_machines_hostname ON machines(hostname);
2887
+ DROP INDEX IF EXISTS idx_machines_single_primary;
2888
+ CREATE UNIQUE INDEX idx_machines_single_primary
2889
+ ON machines(is_primary) WHERE is_primary = 1;
2890
+
2891
+ CREATE TRIGGER IF NOT EXISTS machines_hostname_canonical_insert
2892
+ BEFORE INSERT ON machines
2893
+ WHEN NEW.hostname != lower(rtrim(trim(NEW.hostname), '.'))
2894
+ OR length(NEW.hostname) NOT BETWEEN 1 AND 253
2895
+ OR instr(NEW.hostname, ' ') > 0
2896
+ OR instr(NEW.hostname, char(9)) > 0
2897
+ OR instr(NEW.hostname, char(10)) > 0
2898
+ OR instr(NEW.hostname, char(13)) > 0
2899
+ OR instr(NEW.hostname, '/') > 0
2900
+ OR instr(NEW.hostname, char(92)) > 0
2901
+ BEGIN
2902
+ SELECT RAISE(ABORT, 'machines.hostname must be canonical');
2903
+ END;
2904
+
2905
+ CREATE TRIGGER IF NOT EXISTS machines_hostname_canonical_update
2906
+ BEFORE UPDATE OF hostname ON machines
2907
+ WHEN NEW.hostname != lower(rtrim(trim(NEW.hostname), '.'))
2908
+ OR length(NEW.hostname) NOT BETWEEN 1 AND 253
2909
+ OR instr(NEW.hostname, ' ') > 0
2910
+ OR instr(NEW.hostname, char(9)) > 0
2911
+ OR instr(NEW.hostname, char(10)) > 0
2912
+ OR instr(NEW.hostname, char(13)) > 0
2913
+ OR instr(NEW.hostname, '/') > 0
2914
+ OR instr(NEW.hostname, char(92)) > 0
2915
+ BEGIN
2916
+ SELECT RAISE(ABORT, 'machines.hostname must be canonical');
2917
+ END;
2918
+
2919
+ CREATE TRIGGER IF NOT EXISTS machines_runtime_contract_insert
2920
+ BEFORE INSERT ON machines
2921
+ WHEN length(NEW.name) NOT BETWEEN 1 AND 128
2922
+ OR instr(NEW.name, char(0)) > 0
2923
+ OR instr(NEW.name, char(1)) > 0
2924
+ OR instr(NEW.name, char(2)) > 0
2925
+ OR instr(NEW.name, char(3)) > 0
2926
+ OR instr(NEW.name, char(4)) > 0
2927
+ OR instr(NEW.name, char(5)) > 0
2928
+ OR instr(NEW.name, char(6)) > 0
2929
+ OR instr(NEW.name, char(7)) > 0
2930
+ OR instr(NEW.name, char(8)) > 0
2931
+ OR instr(NEW.name, char(9)) > 0
2932
+ OR instr(NEW.name, char(10)) > 0
2933
+ OR instr(NEW.name, char(11)) > 0
2934
+ OR instr(NEW.name, char(12)) > 0
2935
+ OR instr(NEW.name, char(13)) > 0
2936
+ OR instr(NEW.name, char(14)) > 0
2937
+ OR instr(NEW.name, char(15)) > 0
2938
+ OR instr(NEW.name, char(16)) > 0
2939
+ OR instr(NEW.name, char(17)) > 0
2940
+ OR instr(NEW.name, char(18)) > 0
2941
+ OR instr(NEW.name, char(19)) > 0
2942
+ OR instr(NEW.name, char(20)) > 0
2943
+ OR instr(NEW.name, char(21)) > 0
2944
+ OR instr(NEW.name, char(22)) > 0
2945
+ OR instr(NEW.name, char(23)) > 0
2946
+ OR instr(NEW.name, char(24)) > 0
2947
+ OR instr(NEW.name, char(25)) > 0
2948
+ OR instr(NEW.name, char(26)) > 0
2949
+ OR instr(NEW.name, char(27)) > 0
2950
+ OR instr(NEW.name, char(28)) > 0
2951
+ OR instr(NEW.name, char(29)) > 0
2952
+ OR instr(NEW.name, char(30)) > 0
2953
+ OR instr(NEW.name, char(31)) > 0
2954
+ OR instr(NEW.name, char(127)) > 0
2955
+ OR length(NEW.platform) NOT BETWEEN 1 AND 64
2956
+ OR NEW.platform != lower(trim(NEW.platform))
2957
+ OR NEW.platform GLOB '*[^a-z0-9._-]*'
2958
+ OR NEW.is_primary NOT IN (0, 1)
2959
+ OR julianday(NEW.created_at) IS NULL
2960
+ OR julianday(NEW.last_seen_at) IS NULL
2961
+ OR julianday(NEW.last_seen_at) < julianday(NEW.created_at)
2962
+ BEGIN
2963
+ SELECT RAISE(ABORT, 'machines row violates the persisted runtime contract');
2964
+ END;
2965
+
2966
+ CREATE TRIGGER IF NOT EXISTS machines_runtime_contract_update
2967
+ BEFORE UPDATE OF name, platform, is_primary, created_at, last_seen_at ON machines
2968
+ WHEN length(NEW.name) NOT BETWEEN 1 AND 128
2969
+ OR instr(NEW.name, char(0)) > 0
2970
+ OR instr(NEW.name, char(1)) > 0
2971
+ OR instr(NEW.name, char(2)) > 0
2972
+ OR instr(NEW.name, char(3)) > 0
2973
+ OR instr(NEW.name, char(4)) > 0
2974
+ OR instr(NEW.name, char(5)) > 0
2975
+ OR instr(NEW.name, char(6)) > 0
2976
+ OR instr(NEW.name, char(7)) > 0
2977
+ OR instr(NEW.name, char(8)) > 0
2978
+ OR instr(NEW.name, char(9)) > 0
2979
+ OR instr(NEW.name, char(10)) > 0
2980
+ OR instr(NEW.name, char(11)) > 0
2981
+ OR instr(NEW.name, char(12)) > 0
2982
+ OR instr(NEW.name, char(13)) > 0
2983
+ OR instr(NEW.name, char(14)) > 0
2984
+ OR instr(NEW.name, char(15)) > 0
2985
+ OR instr(NEW.name, char(16)) > 0
2986
+ OR instr(NEW.name, char(17)) > 0
2987
+ OR instr(NEW.name, char(18)) > 0
2988
+ OR instr(NEW.name, char(19)) > 0
2989
+ OR instr(NEW.name, char(20)) > 0
2990
+ OR instr(NEW.name, char(21)) > 0
2991
+ OR instr(NEW.name, char(22)) > 0
2992
+ OR instr(NEW.name, char(23)) > 0
2993
+ OR instr(NEW.name, char(24)) > 0
2994
+ OR instr(NEW.name, char(25)) > 0
2995
+ OR instr(NEW.name, char(26)) > 0
2996
+ OR instr(NEW.name, char(27)) > 0
2997
+ OR instr(NEW.name, char(28)) > 0
2998
+ OR instr(NEW.name, char(29)) > 0
2999
+ OR instr(NEW.name, char(30)) > 0
3000
+ OR instr(NEW.name, char(31)) > 0
3001
+ OR instr(NEW.name, char(127)) > 0
3002
+ OR length(NEW.platform) NOT BETWEEN 1 AND 64
3003
+ OR NEW.platform != lower(trim(NEW.platform))
3004
+ OR NEW.platform GLOB '*[^a-z0-9._-]*'
3005
+ OR NEW.is_primary NOT IN (0, 1)
3006
+ OR julianday(NEW.created_at) IS NULL
3007
+ OR julianday(NEW.last_seen_at) IS NULL
3008
+ OR julianday(NEW.last_seen_at) < julianday(NEW.created_at)
3009
+ BEGIN
3010
+ SELECT RAISE(ABORT, 'machines row violates the persisted runtime contract');
3011
+ END;
3012
+
3013
+ INSERT OR IGNORE INTO _migrations (id) VALUES (41);
2790
3014
  `
2791
3015
  ];
2792
3016
  });
@@ -2802,7 +3026,8 @@ __export(exports_database, {
2802
3026
  getDbPath: () => getDbPath,
2803
3027
  getDatabase: () => getDatabase,
2804
3028
  escapeLikePrefix: () => escapeLikePrefix,
2805
- closeDatabase: () => closeDatabase
3029
+ closeDatabase: () => closeDatabase,
3030
+ applyMachineIdentityMigration: () => applyMachineIdentityMigration
2806
3031
  });
2807
3032
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, cpSync } from "fs";
2808
3033
  import { dirname, join as join4, resolve as resolve2 } from "path";
@@ -2930,32 +3155,62 @@ function getDatabase(dbPath) {
2930
3155
  return _db;
2931
3156
  }
2932
3157
  function runMigrations(db) {
3158
+ let currentLevel = 0;
2933
3159
  try {
2934
3160
  const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
2935
- const currentLevel = result?.max_id ?? 0;
2936
- for (let i = currentLevel;i < MIGRATIONS.length; i++) {
2937
- try {
2938
- applyMigration(db, i);
2939
- } catch (e) {
2940
- console.warn(`[mementos] Migration ${i + 1} failed: ${e instanceof Error ? e.message : String(e)}`);
2941
- }
2942
- }
2943
- } catch {
2944
- for (let i = 0;i < MIGRATIONS.length; i++) {
2945
- try {
2946
- applyMigration(db, i);
2947
- } catch (e) {
2948
- console.warn(`[mementos] Migration ${i + 1} failed: ${e instanceof Error ? e.message : String(e)}`);
3161
+ currentLevel = result?.max_id ?? 0;
3162
+ } catch {}
3163
+ for (let i = currentLevel;i < MIGRATIONS.length; i++) {
3164
+ try {
3165
+ applyMigration(db, i);
3166
+ } catch (error) {
3167
+ const message = error instanceof Error ? error.message : String(error);
3168
+ if (i === 40) {
3169
+ throw new Error(`Mementos migration 41 refused unsafe machine identity data: ${message}`);
2949
3170
  }
3171
+ console.warn(`[mementos] Migration ${i + 1} failed: ${message}`);
2950
3172
  }
2951
3173
  }
2952
3174
  }
2953
3175
  function applyMigration(db, index) {
2954
- if (index === 32) {
3176
+ if (index === 32)
2955
3177
  ensureMachinePrimaryColumn(db);
3178
+ if (index === 40) {
3179
+ applyMachineIdentityMigration(db);
3180
+ return;
2956
3181
  }
2957
3182
  db.exec(MIGRATIONS[index]);
2958
3183
  }
3184
+ function applyMachineIdentityMigration(db) {
3185
+ db.transaction(() => {
3186
+ const rows = db.query("SELECT id, name, hostname, platform, is_primary, created_at, last_seen_at FROM machines").all();
3187
+ const identities = new Map;
3188
+ let primaryCount = 0;
3189
+ for (const row of rows) {
3190
+ const canonical = row.hostname.trim().replace(/\.+$/, "").toLowerCase();
3191
+ const createdAt = new Date(row.created_at).getTime();
3192
+ const lastSeenAt = new Date(row.last_seen_at).getTime();
3193
+ const primaryValueValid = row.is_primary === 0 || row.is_primary === 1 || row.is_primary === false || row.is_primary === true;
3194
+ if (!row.id || !row.name || row.name.length > 128 || /[\u0000-\u001f\u007f]/.test(row.name) || !canonical || canonical.length > 253 || /[\u0000-\u001f\u007f/\\\s]/.test(canonical) || !row.platform || row.platform.length > 64 || row.platform !== row.platform.trim().toLowerCase() || !/^[a-z0-9._-]+$/.test(row.platform) || !primaryValueValid || !Number.isFinite(createdAt) || !Number.isFinite(lastSeenAt) || lastSeenAt < createdAt) {
3195
+ throw new Error(`invalid legacy machine row ${row.id || "(missing id)"}`);
3196
+ }
3197
+ const owner = identities.get(canonical);
3198
+ if (owner && owner !== row.id) {
3199
+ throw new Error(`ambiguous normalized hostname '${canonical}' belongs to both ${owner} and ${row.id}`);
3200
+ }
3201
+ identities.set(canonical, row.id);
3202
+ if (Boolean(row.is_primary))
3203
+ primaryCount += 1;
3204
+ }
3205
+ if (primaryCount > 1) {
3206
+ throw new Error(`legacy machine registry contains ${primaryCount} primary rows`);
3207
+ }
3208
+ db.exec(MIGRATIONS[40]);
3209
+ const receipt = db.query("SELECT id FROM _migrations WHERE id = 41").get();
3210
+ if (receipt?.id !== 41)
3211
+ throw new Error("machine identity migration did not record completion");
3212
+ });
3213
+ }
2959
3214
  function ensureMachinePrimaryColumn(db) {
2960
3215
  const columns = db.query("PRAGMA table_info(machines)").all();
2961
3216
  if (columns.length === 0 || columns.some((column) => column.name === "is_primary")) {
@@ -3100,10 +3355,10 @@ var init_util = __esm(() => {
3100
3355
  return obj[e];
3101
3356
  });
3102
3357
  };
3103
- util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object) => {
3358
+ util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object2) => {
3104
3359
  const keys = [];
3105
- for (const key in object) {
3106
- if (Object.prototype.hasOwnProperty.call(object, key)) {
3360
+ for (const key in object2) {
3361
+ if (Object.prototype.hasOwnProperty.call(object2, key)) {
3107
3362
  keys.push(key);
3108
3363
  }
3109
3364
  }
@@ -7457,19 +7712,19 @@ function floatSafeRemainder2(val, step) {
7457
7712
  const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", ""));
7458
7713
  return valInt % stepInt / 10 ** decCount;
7459
7714
  }
7460
- function defineLazy(object, key, getter) {
7715
+ function defineLazy(object2, key, getter) {
7461
7716
  const set = false;
7462
- Object.defineProperty(object, key, {
7717
+ Object.defineProperty(object2, key, {
7463
7718
  get() {
7464
7719
  if (!set) {
7465
7720
  const value = getter();
7466
- object[key] = value;
7721
+ object2[key] = value;
7467
7722
  return value;
7468
7723
  }
7469
7724
  throw new Error("cached value already set");
7470
7725
  },
7471
7726
  set(v) {
7472
- Object.defineProperty(object, key, {
7727
+ Object.defineProperty(object2, key, {
7473
7728
  value: v
7474
7729
  });
7475
7730
  },
@@ -8085,7 +8340,7 @@ __export(exports_regexes, {
8085
8340
  undefined: () => _undefined,
8086
8341
  ulid: () => ulid,
8087
8342
  time: () => time,
8088
- string: () => string,
8343
+ string: () => string2,
8089
8344
  rfc5322Email: () => rfc5322Email,
8090
8345
  number: () => number,
8091
8346
  null: () => _null,
@@ -8094,7 +8349,7 @@ __export(exports_regexes, {
8094
8349
  ksuid: () => ksuid,
8095
8350
  ipv6: () => ipv6,
8096
8351
  ipv4: () => ipv4,
8097
- integer: () => integer,
8352
+ integer: () => integer2,
8098
8353
  html5Email: () => html5Email,
8099
8354
  hostname: () => hostname2,
8100
8355
  guid: () => guid,
@@ -8111,7 +8366,7 @@ __export(exports_regexes, {
8111
8366
  cidrv6: () => cidrv6,
8112
8367
  cidrv4: () => cidrv4,
8113
8368
  browserEmail: () => browserEmail,
8114
- boolean: () => boolean,
8369
+ boolean: () => boolean2,
8115
8370
  bigint: () => bigint,
8116
8371
  base64url: () => base64url,
8117
8372
  base64: () => base64,
@@ -8142,10 +8397,10 @@ var cuid, cuid2, ulid, xid, ksuid, nanoid, duration, extendedDuration, guid, uui
8142
8397
  if (!version)
8143
8398
  return /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$/;
8144
8399
  return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`);
8145
- }, uuid4, uuid6, uuid7, email, html5Email, rfc5322Email, unicodeEmail, browserEmail, _emoji = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`, ipv4, ipv6, cidrv4, cidrv6, base64, base64url, hostname2, domain, e164, dateSource = `(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))`, date, string = (params) => {
8400
+ }, uuid4, uuid6, uuid7, email, html5Email, rfc5322Email, unicodeEmail, browserEmail, _emoji = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`, ipv4, ipv6, cidrv4, cidrv6, base64, base64url, hostname2, domain, e164, dateSource = `(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))`, date, string2 = (params) => {
8146
8401
  const regex = params ? `[\\s\\S]{${params?.minimum ?? 0},${params?.maximum ?? ""}}` : `[\\s\\S]*`;
8147
8402
  return new RegExp(`^${regex}$`);
8148
- }, bigint, integer, number, boolean, _null, _undefined, lowercase, uppercase;
8403
+ }, bigint, integer2, number, boolean2, _null, _undefined, lowercase, uppercase;
8149
8404
  var init_regexes = __esm(() => {
8150
8405
  cuid = /^[cC][^\s-]{8,}$/;
8151
8406
  cuid2 = /^[0-9a-z]+$/;
@@ -8175,9 +8430,9 @@ var init_regexes = __esm(() => {
8175
8430
  e164 = /^\+(?:[0-9]){6,14}[0-9]$/;
8176
8431
  date = /* @__PURE__ */ new RegExp(`^${dateSource}$`);
8177
8432
  bigint = /^\d+n?$/;
8178
- integer = /^\d+$/;
8433
+ integer2 = /^\d+$/;
8179
8434
  number = /^-?\d+(?:\.\d+)?/i;
8180
- boolean = /true|false/i;
8435
+ boolean2 = /true|false/i;
8181
8436
  _null = /null/i;
8182
8437
  _undefined = /undefined/i;
8183
8438
  lowercase = /^[^A-Z]*$/;
@@ -8296,7 +8551,7 @@ var init_checks = __esm(() => {
8296
8551
  bag.minimum = minimum;
8297
8552
  bag.maximum = maximum;
8298
8553
  if (isInt)
8299
- bag.pattern = integer;
8554
+ bag.pattern = integer2;
8300
8555
  });
8301
8556
  inst._zod.check = (payload) => {
8302
8557
  const input = payload.value;
@@ -9098,7 +9353,7 @@ var init_schemas = __esm(() => {
9098
9353
  });
9099
9354
  $ZodString = /* @__PURE__ */ $constructor("$ZodString", (inst, def) => {
9100
9355
  $ZodType.init(inst, def);
9101
- inst._zod.pattern = [...inst?._zod.bag?.patterns ?? []].pop() ?? string(inst._zod.bag);
9356
+ inst._zod.pattern = [...inst?._zod.bag?.patterns ?? []].pop() ?? string2(inst._zod.bag);
9102
9357
  inst._zod.parse = (payload, _) => {
9103
9358
  if (def.coerce)
9104
9359
  try {
@@ -9398,7 +9653,7 @@ var init_schemas = __esm(() => {
9398
9653
  });
9399
9654
  $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
9400
9655
  $ZodType.init(inst, def);
9401
- inst._zod.pattern = boolean;
9656
+ inst._zod.pattern = boolean2;
9402
9657
  inst._zod.parse = (payload, _ctx) => {
9403
9658
  if (def.coerce)
9404
9659
  try {
@@ -17422,7 +17677,7 @@ var init_parse2 = __esm(() => {
17422
17677
  });
17423
17678
 
17424
17679
  // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/classic/schemas.js
17425
- function string2(params) {
17680
+ function string3(params) {
17426
17681
  return _string(ZodString2, params);
17427
17682
  }
17428
17683
  function email2(params) {
@@ -17512,7 +17767,7 @@ function int32(params) {
17512
17767
  function uint32(params) {
17513
17768
  return _uint32(ZodNumberFormat, params);
17514
17769
  }
17515
- function boolean2(params) {
17770
+ function boolean3(params) {
17516
17771
  return _boolean(ZodBoolean2, params);
17517
17772
  }
17518
17773
  function bigint2(params) {
@@ -17555,7 +17810,7 @@ function keyof(schema) {
17555
17810
  const shape = schema._zod.def.shape;
17556
17811
  return literal(Object.keys(shape));
17557
17812
  }
17558
- function object(shape, params) {
17813
+ function object2(shape, params) {
17559
17814
  const def = {
17560
17815
  type: "object",
17561
17816
  get shape() {
@@ -17621,7 +17876,7 @@ function tuple(items, _paramsOrRest, _params) {
17621
17876
  ...exports_util.normalizeParams(params)
17622
17877
  });
17623
17878
  }
17624
- function record(keyType, valueType, params) {
17879
+ function record2(keyType, valueType, params) {
17625
17880
  return new ZodRecord2({
17626
17881
  type: "record",
17627
17882
  keyType,
@@ -17819,7 +18074,7 @@ function _instanceof(cls, params = {
17819
18074
  }
17820
18075
  function json2(params) {
17821
18076
  const jsonSchema = lazy(() => {
17822
- return union([string2(params), number2(), boolean2(), _null3(), array(jsonSchema), record(string2(), jsonSchema)]);
18077
+ return union([string3(params), number2(), boolean3(), _null3(), array(jsonSchema), record2(string3(), jsonSchema)]);
17823
18078
  });
17824
18079
  return jsonSchema;
17825
18080
  }
@@ -18390,19 +18645,19 @@ var init_compat = __esm(() => {
18390
18645
  // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v4/classic/coerce.js
18391
18646
  var exports_coerce = {};
18392
18647
  __export(exports_coerce, {
18393
- string: () => string3,
18648
+ string: () => string4,
18394
18649
  number: () => number3,
18395
18650
  date: () => date4,
18396
- boolean: () => boolean3,
18651
+ boolean: () => boolean4,
18397
18652
  bigint: () => bigint3
18398
18653
  });
18399
- function string3(params) {
18654
+ function string4(params) {
18400
18655
  return _coercedString(ZodString2, params);
18401
18656
  }
18402
18657
  function number3(params) {
18403
18658
  return _coercedNumber(ZodNumber2, params);
18404
18659
  }
18405
- function boolean3(params) {
18660
+ function boolean4(params) {
18406
18661
  return _coercedBoolean(ZodBoolean2, params);
18407
18662
  }
18408
18663
  function bigint3(params) {
@@ -18446,7 +18701,7 @@ __export(exports_external2, {
18446
18701
  success: () => success,
18447
18702
  stringbool: () => stringbool,
18448
18703
  stringFormat: () => stringFormat,
18449
- string: () => string2,
18704
+ string: () => string3,
18450
18705
  strictObject: () => strictObject,
18451
18706
  startsWith: () => _startsWith,
18452
18707
  size: () => _size,
@@ -18458,7 +18713,7 @@ __export(exports_external2, {
18458
18713
  regexes: () => exports_regexes,
18459
18714
  regex: () => _regex,
18460
18715
  refine: () => refine,
18461
- record: () => record,
18716
+ record: () => record2,
18462
18717
  readonly: () => readonly,
18463
18718
  property: () => _property,
18464
18719
  promise: () => promise,
@@ -18472,7 +18727,7 @@ __export(exports_external2, {
18472
18727
  parse: () => parse3,
18473
18728
  overwrite: () => _overwrite,
18474
18729
  optional: () => optional,
18475
- object: () => object,
18730
+ object: () => object2,
18476
18731
  number: () => number2,
18477
18732
  nullish: () => nullish2,
18478
18733
  nullable: () => nullable,
@@ -18543,7 +18798,7 @@ __export(exports_external2, {
18543
18798
  cidrv4: () => cidrv42,
18544
18799
  check: () => check,
18545
18800
  catch: () => _catch2,
18546
- boolean: () => boolean2,
18801
+ boolean: () => boolean3,
18547
18802
  bigint: () => bigint2,
18548
18803
  base64url: () => base64url2,
18549
18804
  base64: () => base642,
@@ -20056,11 +20311,11 @@ function parseMapDef(def, refs) {
20056
20311
  };
20057
20312
  }
20058
20313
  function parseNativeEnumDef(def) {
20059
- const object2 = def.values;
20314
+ const object3 = def.values;
20060
20315
  const actualKeys = Object.keys(def.values).filter((key) => {
20061
- return typeof object2[object2[key]] !== "number";
20316
+ return typeof object3[object3[key]] !== "number";
20062
20317
  });
20063
- const actualValues = actualKeys.map((key) => object2[key]);
20318
+ const actualValues = actualKeys.map((key) => object3[key]);
20064
20319
  const parsedTypes = Array.from(new Set(actualValues.map((values) => typeof values)));
20065
20320
  return {
20066
20321
  type: parsedTypes.length === 1 ? parsedTypes[0] === "string" ? "string" : "number" : ["string", "number"],
@@ -35417,7 +35672,7 @@ var require_token_io = __commonJS((exports, module) => {
35417
35672
  module.exports = __toCommonJS(token_io_exports);
35418
35673
  var import_path = __toESM2(__require("path"));
35419
35674
  var import_fs = __toESM2(__require("fs"));
35420
- var import_os2 = __toESM2(__require("os"));
35675
+ var import_os = __toESM2(__require("os"));
35421
35676
  var import_token_error = require_token_error();
35422
35677
  function findRootDir() {
35423
35678
  try {
@@ -35438,11 +35693,11 @@ var require_token_io = __commonJS((exports, module) => {
35438
35693
  if (process.env.XDG_DATA_HOME) {
35439
35694
  return process.env.XDG_DATA_HOME;
35440
35695
  }
35441
- switch (import_os2.default.platform()) {
35696
+ switch (import_os.default.platform()) {
35442
35697
  case "darwin":
35443
- return import_path.default.join(import_os2.default.homedir(), "Library/Application Support");
35698
+ return import_path.default.join(import_os.default.homedir(), "Library/Application Support");
35444
35699
  case "linux":
35445
- return import_path.default.join(import_os2.default.homedir(), ".local/share");
35700
+ return import_path.default.join(import_os.default.homedir(), ".local/share");
35446
35701
  case "win32":
35447
35702
  if (process.env.LOCALAPPDATA) {
35448
35703
  return process.env.LOCALAPPDATA;
@@ -35552,10 +35807,10 @@ var require_oauth = __commonJS((exports, module) => {
35552
35807
  refreshTokenRequest: () => refreshTokenRequest
35553
35808
  });
35554
35809
  module.exports = __toCommonJS(oauth_exports);
35555
- var import_os2 = __require("os");
35810
+ var import_os = __require("os");
35556
35811
  var VERCEL_ISSUER = "https://vercel.com";
35557
35812
  var VERCEL_CLI_CLIENT_ID = "cl_HYyOPBNtFMfHhaUn9L4QPfTZz6TP47bp";
35558
- var userAgent = `@vercel/oidc node-${process.version} ${(0, import_os2.platform)()} (${(0, import_os2.arch)()}) ${(0, import_os2.hostname)()}`;
35813
+ var userAgent = `@vercel/oidc node-${process.version} ${(0, import_os.platform)()} (${(0, import_os.arch)()}) ${(0, import_os.hostname)()}`;
35559
35814
  var _tokenEndpoint = null;
35560
35815
  async function getTokenEndpoint() {
35561
35816
  if (_tokenEndpoint) {
@@ -40695,8 +40950,8 @@ function prepareCallSettings({
40695
40950
  seed
40696
40951
  };
40697
40952
  }
40698
- function isNonEmptyObject(object2) {
40699
- return object2 != null && Object.keys(object2).length > 0;
40953
+ function isNonEmptyObject(object22) {
40954
+ return object22 != null && Object.keys(object22).length > 0;
40700
40955
  }
40701
40956
  async function prepareToolsAndToolChoice({
40702
40957
  tools,
@@ -47784,7 +48039,7 @@ var import_api2, import_api3, __defProp2, __export2 = (target, all) => {
47784
48039
  createElementStreamTransform() {
47785
48040
  return;
47786
48041
  }
47787
- }), object2 = ({
48042
+ }), object3 = ({
47788
48043
  schema: inputSchema,
47789
48044
  name: name232,
47790
48045
  description
@@ -52522,7 +52777,7 @@ var init_dist8 = __esm(() => {
52522
52777
  array: () => array2,
52523
52778
  choice: () => choice,
52524
52779
  json: () => json3,
52525
- object: () => object2,
52780
+ object: () => object3,
52526
52781
  text: () => text
52527
52782
  });
52528
52783
  originalGenerateId = createIdGenerator({
@@ -54008,7 +54263,7 @@ function buildMemoryListConditions(filter) {
54008
54263
  conditions.push("status = 'active'");
54009
54264
  }
54010
54265
  if (filter.project_id) {
54011
- conditions.push("project_id = ?");
54266
+ conditions.push(filter.include_unassigned_project === true ? "(project_id = ? OR project_id IS NULL)" : "project_id = ?");
54012
54267
  params.push(filter.project_id);
54013
54268
  }
54014
54269
  if (filter.agent_id) {
@@ -54088,6 +54343,7 @@ function listMemoriesPage(filter, db) {
54088
54343
  pinned: f.pinned,
54089
54344
  agent_id: f.agent_id,
54090
54345
  project_id: f.project_id,
54346
+ include_unassigned_project: f.project_id && f.include_unassigned_project === true ? true : undefined,
54091
54347
  session_id: f.session_id,
54092
54348
  machine_id: f.machine_id,
54093
54349
  visible_to_machine_id: f.visible_to_machine_id,
@@ -54112,7 +54368,7 @@ function listMemoriesPage(filter, db) {
54112
54368
  if (conditions.length > 0) {
54113
54369
  sql += ` WHERE ${conditions.join(" AND ")}`;
54114
54370
  }
54115
- sql += " ORDER BY importance DESC, created_at DESC";
54371
+ sql += " ORDER BY importance DESC, created_at DESC, id DESC";
54116
54372
  if (f.limit) {
54117
54373
  sql += " LIMIT ?";
54118
54374
  params.push(f.limit);
@@ -55435,17 +55691,83 @@ function updateAgent(id, updates, db) {
55435
55691
  // src/db/locks.ts
55436
55692
  init_database();
55437
55693
  init_api_mode();
55438
- function parseLockRow(row) {
55694
+
55695
+ // src/db/api-response-contract.ts
55696
+ class MementosApiProtocolError extends Error {
55697
+ code = "MEMENTOS_API_PROTOCOL";
55698
+ constructor(operation, detail) {
55699
+ super(`mementos cloud ${operation} returned a malformed 2xx response: ${detail}`);
55700
+ this.name = "MementosApiProtocolError";
55701
+ }
55702
+ }
55703
+ function expectObject(value, operation) {
55704
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
55705
+ throw new MementosApiProtocolError(operation, "expected a JSON object");
55706
+ }
55707
+ return value;
55708
+ }
55709
+ function expectArray(value, operation, field) {
55710
+ if (!Array.isArray(value)) {
55711
+ throw new MementosApiProtocolError(operation, field ? `expected '${field}' to be an array` : "expected a JSON array");
55712
+ }
55713
+ return value;
55714
+ }
55715
+ function expectString(object, field, operation, options = {}) {
55716
+ const value = object[field];
55717
+ if (typeof value !== "string" || !options.allowEmpty && value.length === 0) {
55718
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be ${options.allowEmpty ? "a string" : "a non-empty string"}`);
55719
+ }
55720
+ return value;
55721
+ }
55722
+ function expectBoolean(object, field, operation) {
55723
+ const value = object[field];
55724
+ if (typeof value !== "boolean") {
55725
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a boolean`);
55726
+ }
55727
+ return value;
55728
+ }
55729
+ function expectNonNegativeInteger(object, field, operation) {
55730
+ const value = object[field];
55731
+ if (!Number.isSafeInteger(value) || value < 0) {
55732
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a non-negative safe integer`);
55733
+ }
55734
+ return value;
55735
+ }
55736
+
55737
+ // src/db/locks.ts
55738
+ var RESOURCE_TYPES = new Set(["project", "memory", "entity", "agent", "connector", "file"]);
55739
+ var LOCK_TYPES = new Set(["advisory", "exclusive"]);
55740
+ function parseLockRow(row, operation = "lock response") {
55741
+ const object = expectObject(row, operation);
55742
+ const resourceType = expectString(object, "resource_type", operation);
55743
+ const lockType = expectString(object, "lock_type", operation);
55744
+ if (!RESOURCE_TYPES.has(resourceType)) {
55745
+ throw new MementosApiProtocolError(operation, "unsupported 'resource_type'");
55746
+ }
55747
+ if (!LOCK_TYPES.has(lockType)) {
55748
+ throw new MementosApiProtocolError(operation, "unsupported 'lock_type'");
55749
+ }
55750
+ const timestamp = (field) => {
55751
+ const value = object[field];
55752
+ const date = value instanceof Date ? value : new Date(typeof value === "string" ? value : Number.NaN);
55753
+ if (!Number.isFinite(date.getTime())) {
55754
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a timestamp`);
55755
+ }
55756
+ return date.toISOString();
55757
+ };
55439
55758
  return {
55440
- id: row["id"],
55441
- resource_type: row["resource_type"],
55442
- resource_id: row["resource_id"],
55443
- agent_id: row["agent_id"],
55444
- lock_type: row["lock_type"],
55445
- locked_at: row["locked_at"],
55446
- expires_at: row["expires_at"]
55759
+ id: expectString(object, "id", operation),
55760
+ resource_type: resourceType,
55761
+ resource_id: expectString(object, "resource_id", operation),
55762
+ agent_id: expectString(object, "agent_id", operation),
55763
+ lock_type: lockType,
55764
+ locked_at: timestamp("locked_at"),
55765
+ expires_at: timestamp("expires_at")
55447
55766
  };
55448
55767
  }
55768
+ function parseLockList(value, operation) {
55769
+ return expectArray(value, operation).map((row, index) => parseLockRow(row, `${operation} item ${index}`));
55770
+ }
55449
55771
  function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive", ttlSeconds = 300, db) {
55450
55772
  if (!db && isApiMode()) {
55451
55773
  try {
@@ -55456,7 +55778,7 @@ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive",
55456
55778
  lock_type: lockType,
55457
55779
  ttl_seconds: ttlSeconds
55458
55780
  });
55459
- return data;
55781
+ return parseLockRow(data, "POST /locks");
55460
55782
  } catch (e) {
55461
55783
  if (e instanceof ApiRequestError && e.status === 409)
55462
55784
  return null;
@@ -55465,7 +55787,7 @@ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive",
55465
55787
  }
55466
55788
  const d = db || getDatabase();
55467
55789
  cleanExpiredLocks(d);
55468
- const ownLock = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND agent_id = ? AND lock_type = ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')").get(resourceType, resourceId, agentId, lockType);
55790
+ const ownLock = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND agent_id = ? AND lock_type = ? AND expires_at > ?").get(resourceType, resourceId, agentId, lockType, now());
55469
55791
  if (ownLock) {
55470
55792
  const newExpiry = new Date(Date.now() + ttlSeconds * 1000).toISOString();
55471
55793
  d.run("UPDATE resource_locks SET expires_at = ? WHERE id = ?", [
@@ -55475,7 +55797,7 @@ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive",
55475
55797
  return parseLockRow({ ...ownLock, expires_at: newExpiry });
55476
55798
  }
55477
55799
  if (lockType === "exclusive") {
55478
- const existing = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = 'exclusive' AND agent_id != ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')").get(resourceType, resourceId, agentId);
55800
+ const existing = d.query("SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = 'exclusive' AND agent_id != ? AND expires_at > ?").get(resourceType, resourceId, agentId, now());
55479
55801
  if (existing) {
55480
55802
  return null;
55481
55803
  }
@@ -55496,8 +55818,11 @@ function acquireLock(agentId, resourceType, resourceId, lockType = "exclusive",
55496
55818
  }
55497
55819
  function releaseLock(lockId, agentId, db) {
55498
55820
  if (!db && isApiMode()) {
55499
- const { status } = apiJson("DELETE", `/locks/${encodeURIComponent(lockId)}`, { agent_id: agentId }, { allow404: true });
55500
- return status !== 404;
55821
+ const operation = `DELETE /locks/${encodeURIComponent(lockId)}`;
55822
+ const { status, data } = apiJson("DELETE", `/locks/${encodeURIComponent(lockId)}`, { agent_id: agentId }, { allow404: true });
55823
+ if (status === 404)
55824
+ return false;
55825
+ return expectBoolean(expectObject(data, operation), "released", operation);
55501
55826
  }
55502
55827
  const d = db || getDatabase();
55503
55828
  const result = d.run("DELETE FROM resource_locks WHERE id = ? AND agent_id = ?", [lockId, agentId]);
@@ -55510,8 +55835,9 @@ function releaseResourceLocks(agentId, resourceType, resourceId, db) {
55510
55835
  }
55511
55836
  function releaseAllAgentLocks(agentId, db) {
55512
55837
  if (!db && isApiMode()) {
55838
+ const operation = `DELETE /agents/${encodeURIComponent(agentId)}/locks`;
55513
55839
  const { data } = apiJson("DELETE", `/agents/${encodeURIComponent(agentId)}/locks`);
55514
- return data?.released ?? 0;
55840
+ return expectNonNegativeInteger(expectObject(data, operation), "released", operation);
55515
55841
  }
55516
55842
  const d = db || getDatabase();
55517
55843
  const result = d.run("DELETE FROM resource_locks WHERE agent_id = ?", [agentId]);
@@ -55521,69 +55847,714 @@ function checkLock(resourceType, resourceId, lockType, db) {
55521
55847
  if (!db && isApiMode()) {
55522
55848
  const q = toQuery({ resource_type: resourceType, resource_id: resourceId, lock_type: lockType });
55523
55849
  const { data } = apiJson("GET", `/locks${q}`);
55524
- return data ?? [];
55850
+ return parseLockList(data, "GET /locks");
55525
55851
  }
55526
55852
  const d = db || getDatabase();
55527
55853
  cleanExpiredLocks(d);
55528
- const query = lockType ? "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')" : "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
55529
- const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType) : d.query(query).all(resourceType, resourceId);
55530
- return rows.map(parseLockRow);
55854
+ const query = lockType ? "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > ?" : "SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND expires_at > ?";
55855
+ const rows = lockType ? d.query(query).all(resourceType, resourceId, lockType, now()) : d.query(query).all(resourceType, resourceId, now());
55856
+ return rows.map((row) => parseLockRow(row));
55531
55857
  }
55532
55858
  function agentHoldsLock(agentId, resourceType, resourceId, lockType, db) {
55859
+ if (!db && isApiMode()) {
55860
+ const locks = checkLock(resourceType, resourceId, lockType, db);
55861
+ return locks.find((l) => l.agent_id === agentId) ?? null;
55862
+ }
55533
55863
  const d = db || getDatabase();
55534
- const query = lockType ? "SELECT * FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')" : "SELECT * FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
55535
- const row = lockType ? d.query(query).get(agentId, resourceType, resourceId, lockType) : d.query(query).get(agentId, resourceType, resourceId);
55864
+ const query = lockType ? "SELECT * FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ? AND lock_type = ? AND expires_at > ?" : "SELECT * FROM resource_locks WHERE agent_id = ? AND resource_type = ? AND resource_id = ? AND expires_at > ?";
55865
+ const row = lockType ? d.query(query).get(agentId, resourceType, resourceId, lockType, now()) : d.query(query).get(agentId, resourceType, resourceId, now());
55536
55866
  return row ? parseLockRow(row) : null;
55537
55867
  }
55538
55868
  function listAgentLocks(agentId, db) {
55539
55869
  if (!db && isApiMode()) {
55870
+ const operation = `GET /agents/${encodeURIComponent(agentId)}/locks`;
55540
55871
  const { data } = apiJson("GET", `/agents/${encodeURIComponent(agentId)}/locks`);
55541
- return data ?? [];
55872
+ return parseLockList(data, operation);
55542
55873
  }
55543
55874
  const d = db || getDatabase();
55544
55875
  cleanExpiredLocks(d);
55545
- const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now') ORDER BY locked_at DESC").all(agentId);
55546
- return rows.map(parseLockRow);
55876
+ const rows = d.query("SELECT * FROM resource_locks WHERE agent_id = ? AND expires_at > ? ORDER BY locked_at DESC").all(agentId, now());
55877
+ return rows.map((row) => parseLockRow(row));
55547
55878
  }
55548
55879
  function cleanExpiredLocks(db) {
55549
55880
  if (!db && isApiMode()) {
55881
+ const operation = "POST /locks/clean";
55550
55882
  const { data } = apiJson("POST", "/locks/clean");
55551
- return data?.cleaned ?? 0;
55883
+ return expectNonNegativeInteger(expectObject(data, operation), "cleaned", operation);
55552
55884
  }
55553
55885
  const d = db || getDatabase();
55554
- const result = d.run("DELETE FROM resource_locks WHERE expires_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')");
55886
+ const result = d.run("DELETE FROM resource_locks WHERE expires_at <= ?", now());
55555
55887
  return result.changes;
55556
55888
  }
55557
- // src/lib/memory-lock.ts
55889
+ // src/db/audit.ts
55890
+ init_storage();
55558
55891
  init_database();
55892
+ init_api_mode();
55893
+ import { createHash as createHash2, createHmac, randomBytes, timingSafeEqual } from "crypto";
55894
+
55895
+ // src/audit-contract.ts
55896
+ var AUDIT_TRAIL_CONTRACT = "mementos.audit.trail.v1";
55897
+ var AUDIT_EXPORT_CONTRACT = "mementos.audit.export.v1";
55898
+ var AUDIT_STATS_CONTRACT = "mementos.audit.stats.v1";
55899
+ var AUDIT_CURSOR_VERSION = 1;
55900
+ var AUDIT_DEFAULT_LIMIT = 50;
55901
+ var AUDIT_OPERATIONS = [
55902
+ "create",
55903
+ "update",
55904
+ "delete",
55905
+ "archive",
55906
+ "restore",
55907
+ "read"
55908
+ ];
55909
+
55910
+ class AuditContractError extends Error {
55911
+ code = "MEMENTOS_AUDIT_CONTRACT";
55912
+ constructor(message) {
55913
+ super(message);
55914
+ this.name = "AuditContractError";
55915
+ }
55916
+ }
55917
+ var ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
55918
+ var MD5_HEX = /^[0-9a-f]{32}$/;
55919
+ var ENTRY_KEYS = new Set([
55920
+ "id",
55921
+ "memory_id",
55922
+ "memory_key",
55923
+ "operation",
55924
+ "agent_id",
55925
+ "old_value_hash",
55926
+ "new_value_hash",
55927
+ "changes",
55928
+ "created_at"
55929
+ ]);
55930
+ var PAGE_KEYS = new Set([
55931
+ "contract",
55932
+ "entries",
55933
+ "count",
55934
+ "total",
55935
+ "limit",
55936
+ "cursor",
55937
+ "next_cursor",
55938
+ "consumed",
55939
+ "has_more",
55940
+ "complete",
55941
+ "snapshot_at",
55942
+ "filters",
55943
+ "sort"
55944
+ ]);
55945
+ function object(value, label) {
55946
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
55947
+ throw new AuditContractError(`${label} must be a JSON object`);
55948
+ }
55949
+ return value;
55950
+ }
55951
+ function exactKeys(value, allowed, label) {
55952
+ const unexpected = Object.keys(value).filter((key) => !allowed.has(key));
55953
+ if (unexpected.length)
55954
+ throw new AuditContractError(`${label} has unexpected field '${unexpected[0]}'`);
55955
+ }
55956
+ function string(value, label, options = {}) {
55957
+ if (options.nullable && value === null)
55958
+ return null;
55959
+ if (typeof value !== "string" || value.length === 0 || value.length > (options.max ?? 2048) || /[\u0000-\u001f\u007f]/.test(value)) {
55960
+ throw new AuditContractError(`${label} must be a non-empty printable string of at most ${options.max ?? 2048} characters`);
55961
+ }
55962
+ return value;
55963
+ }
55964
+ function cursorString(value, label) {
55965
+ if (value === null)
55966
+ return null;
55967
+ if (typeof value !== "string" || !value || value.length > 4096 || !/^[A-Za-z0-9_-]+$/.test(value)) {
55968
+ throw new AuditContractError(`${label} must be a bounded base64url cursor or null`);
55969
+ }
55970
+ return value;
55971
+ }
55972
+ function integer(value, label, minimum = 0) {
55973
+ if (!Number.isSafeInteger(value) || value < minimum) {
55974
+ throw new AuditContractError(`${label} must be a safe integer >= ${minimum}`);
55975
+ }
55976
+ return value;
55977
+ }
55978
+ function boolean(value, label) {
55979
+ if (typeof value !== "boolean")
55980
+ throw new AuditContractError(`${label} must be a boolean`);
55981
+ return value;
55982
+ }
55983
+ function canonicalAuditTimestamp(value, label) {
55984
+ if (value instanceof Date)
55985
+ value = value.toISOString();
55986
+ if (typeof value !== "string")
55987
+ throw new AuditContractError(`${label} must be a canonical UTC timestamp`);
55988
+ const candidate = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,3})?$/.test(value) ? `${value.replace(" ", "T")}${value.includes(".") ? "" : ".000"}Z` : value;
55989
+ if (!ISO_UTC.test(candidate))
55990
+ throw new AuditContractError(`${label} must use YYYY-MM-DDTHH:mm:ss.sssZ`);
55991
+ const parsed = new Date(candidate);
55992
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== candidate) {
55993
+ throw new AuditContractError(`${label} must be a real calendar timestamp`);
55994
+ }
55995
+ return candidate;
55996
+ }
55997
+ function auditOperation(value, label = "operation") {
55998
+ if (typeof value !== "string" || !AUDIT_OPERATIONS.includes(value)) {
55999
+ throw new AuditContractError(`${label} must be one of: ${AUDIT_OPERATIONS.join(", ")}`);
56000
+ }
56001
+ return value;
56002
+ }
56003
+ function nullableHash(value, label) {
56004
+ if (value === null)
56005
+ return null;
56006
+ if (typeof value !== "string" || !MD5_HEX.test(value)) {
56007
+ throw new AuditContractError(`${label} must be a lowercase md5 hex digest or null`);
56008
+ }
56009
+ return value;
56010
+ }
56011
+ function record(value, label) {
56012
+ return object(value, label);
56013
+ }
56014
+ function validateAuditEntry(value, label = "audit entry") {
56015
+ const row = object(value, label);
56016
+ exactKeys(row, ENTRY_KEYS, label);
56017
+ return {
56018
+ id: string(row.id, `${label}.id`, { max: 512 }),
56019
+ memory_id: string(row.memory_id, `${label}.memory_id`, { max: 512 }),
56020
+ memory_key: string(row.memory_key, `${label}.memory_key`, { nullable: true, max: 4096 }),
56021
+ operation: auditOperation(row.operation, `${label}.operation`),
56022
+ agent_id: string(row.agent_id, `${label}.agent_id`, { nullable: true, max: 512 }),
56023
+ old_value_hash: nullableHash(row.old_value_hash, `${label}.old_value_hash`),
56024
+ new_value_hash: nullableHash(row.new_value_hash, `${label}.new_value_hash`),
56025
+ changes: record(row.changes, `${label}.changes`),
56026
+ created_at: canonicalAuditTimestamp(row.created_at, `${label}.created_at`)
56027
+ };
56028
+ }
56029
+ function nullableTimestamp2(value, label) {
56030
+ return value === null ? null : canonicalAuditTimestamp(value, label);
56031
+ }
56032
+ function validateFilters(value) {
56033
+ const filters = object(value, "audit page filters");
56034
+ const expected = new Set(["memory_id", "since", "until", "operation", "agent_id"]);
56035
+ exactKeys(filters, expected, "audit page filters");
56036
+ const result = {
56037
+ memory_id: string(filters.memory_id, "filters.memory_id", { nullable: true, max: 512 }),
56038
+ since: nullableTimestamp2(filters.since, "filters.since"),
56039
+ until: nullableTimestamp2(filters.until, "filters.until"),
56040
+ operation: filters.operation === null ? null : auditOperation(filters.operation, "filters.operation"),
56041
+ agent_id: string(filters.agent_id, "filters.agent_id", { nullable: true, max: 512 })
56042
+ };
56043
+ if (result.since && result.until && result.since > result.until) {
56044
+ throw new AuditContractError("filters.since must not be after filters.until");
56045
+ }
56046
+ return result;
56047
+ }
56048
+ function compareEntryOrder(left, right) {
56049
+ if (left.created_at !== right.created_at)
56050
+ return left.created_at > right.created_at ? -1 : 1;
56051
+ return left.id > right.id ? -1 : left.id < right.id ? 1 : 0;
56052
+ }
56053
+ function validateAuditPage(value, expected) {
56054
+ const page = object(value, "audit page");
56055
+ exactKeys(page, PAGE_KEYS, "audit page");
56056
+ if (page.contract !== expected.contract)
56057
+ throw new AuditContractError(`expected contract '${expected.contract}'`);
56058
+ const entries = Array.isArray(page.entries) ? page.entries.map((entry, index) => validateAuditEntry(entry, `entries[${index}]`)) : (() => {
56059
+ throw new AuditContractError("entries must be an array");
56060
+ })();
56061
+ const count = integer(page.count, "count");
56062
+ const total = integer(page.total, "total");
56063
+ const limit = integer(page.limit, "limit", 1);
56064
+ const requestedLimit = integer(expected.limit, "requested limit", 1);
56065
+ if (limit !== requestedLimit)
56066
+ throw new AuditContractError("limit receipt does not match the request");
56067
+ const consumed = integer(page.consumed, "consumed");
56068
+ const cursor = cursorString(page.cursor, "cursor");
56069
+ const nextCursor = cursorString(page.next_cursor, "next_cursor");
56070
+ const hasMore = boolean(page.has_more, "has_more");
56071
+ const complete = boolean(page.complete, "complete");
56072
+ const snapshotAt = canonicalAuditTimestamp(page.snapshot_at, "snapshot_at");
56073
+ const filters = validateFilters(page.filters);
56074
+ const sort = object(page.sort, "sort");
56075
+ exactKeys(sort, new Set(["field", "direction", "tie_breaker"]), "sort");
56076
+ if (sort.field !== "created_at" || sort.direction !== "desc" || sort.tie_breaker !== "id") {
56077
+ throw new AuditContractError("sort must be created_at desc with id tie-breaker");
56078
+ }
56079
+ if (cursor !== expected.cursor)
56080
+ throw new AuditContractError("cursor receipt does not match the request");
56081
+ for (const key of ["memory_id", "since", "until", "operation", "agent_id"]) {
56082
+ if (filters[key] !== expected.filters[key]) {
56083
+ throw new AuditContractError(`filters.${key} receipt does not match the request`);
56084
+ }
56085
+ }
56086
+ if (count !== entries.length || count > limit || total < count || consumed < count || consumed > total) {
56087
+ throw new AuditContractError("count/limit/consumed/total fields are inconsistent");
56088
+ }
56089
+ if (hasMore !== consumed < total)
56090
+ throw new AuditContractError("has_more does not match consumed/total");
56091
+ if (hasMore !== (nextCursor !== null))
56092
+ throw new AuditContractError("next_cursor does not match has_more");
56093
+ if (total > 0 && count === 0)
56094
+ throw new AuditContractError("non-empty audit result must make progress on every page");
56095
+ if (hasMore && nextCursor === cursor)
56096
+ throw new AuditContractError("next_cursor must advance beyond the request cursor");
56097
+ const shouldBeComplete = cursor === null && !hasMore && consumed === total;
56098
+ if (complete !== shouldBeComplete)
56099
+ throw new AuditContractError("complete is not truthful for this page");
56100
+ if (cursor === null && consumed !== count)
56101
+ throw new AuditContractError("initial page consumed must equal count");
56102
+ for (let index = 1;index < entries.length; index++) {
56103
+ if (compareEntryOrder(entries[index - 1], entries[index]) >= 0) {
56104
+ throw new AuditContractError("entries are not strictly ordered by created_at desc, id desc");
56105
+ }
56106
+ }
56107
+ const ids = new Set;
56108
+ for (const entry of entries) {
56109
+ if (ids.has(entry.id))
56110
+ throw new AuditContractError(`duplicate audit entry id '${entry.id}'`);
56111
+ ids.add(entry.id);
56112
+ if (filters.memory_id && entry.memory_id !== filters.memory_id)
56113
+ throw new AuditContractError("entry violates memory_id filter");
56114
+ if (filters.operation && entry.operation !== filters.operation)
56115
+ throw new AuditContractError("entry violates operation filter");
56116
+ if (filters.agent_id && entry.agent_id !== filters.agent_id)
56117
+ throw new AuditContractError("entry violates agent_id filter");
56118
+ if (filters.since && entry.created_at < filters.since)
56119
+ throw new AuditContractError("entry violates since filter");
56120
+ if (filters.until && entry.created_at > filters.until)
56121
+ throw new AuditContractError("entry violates until filter");
56122
+ if (entry.created_at > snapshotAt)
56123
+ throw new AuditContractError("entry is newer than the snapshot boundary");
56124
+ }
56125
+ return {
56126
+ contract: expected.contract,
56127
+ entries,
56128
+ count,
56129
+ total,
56130
+ limit,
56131
+ cursor,
56132
+ next_cursor: nextCursor,
56133
+ consumed,
56134
+ has_more: hasMore,
56135
+ complete,
56136
+ snapshot_at: snapshotAt,
56137
+ filters,
56138
+ sort: { field: "created_at", direction: "desc", tie_breaker: "id" }
56139
+ };
56140
+ }
56141
+ function validateAuditStats(value) {
56142
+ const stats = object(value, "audit stats");
56143
+ exactKeys(stats, new Set(["contract", "total_entries", "by_operation", "recent_24h", "snapshot_at"]), "audit stats");
56144
+ if (stats.contract !== AUDIT_STATS_CONTRACT)
56145
+ throw new AuditContractError(`expected contract '${AUDIT_STATS_CONTRACT}'`);
56146
+ const total = integer(stats.total_entries, "total_entries");
56147
+ const recent = integer(stats.recent_24h, "recent_24h");
56148
+ const byOperationObject = object(stats.by_operation, "by_operation");
56149
+ exactKeys(byOperationObject, new Set(AUDIT_OPERATIONS), "by_operation");
56150
+ const byOperation = Object.fromEntries(AUDIT_OPERATIONS.map((operation) => [operation, integer(byOperationObject[operation], `by_operation.${operation}`)]));
56151
+ if (Object.values(byOperation).reduce((sum, count) => sum + count, 0) !== total) {
56152
+ throw new AuditContractError("by_operation counts do not sum to total_entries");
56153
+ }
56154
+ if (recent > total)
56155
+ throw new AuditContractError("recent_24h exceeds total_entries");
56156
+ return {
56157
+ contract: AUDIT_STATS_CONTRACT,
56158
+ total_entries: total,
56159
+ by_operation: byOperation,
56160
+ recent_24h: recent,
56161
+ snapshot_at: canonicalAuditTimestamp(stats.snapshot_at, "snapshot_at")
56162
+ };
56163
+ }
56164
+
56165
+ // src/db/audit.ts
56166
+ var DEFAULT_TRAIL_LIMIT = AUDIT_DEFAULT_LIMIT;
56167
+ var DEFAULT_EXPORT_LIMIT = AUDIT_DEFAULT_LIMIT;
56168
+ var MAX_AUDIT_LIMIT = 1000;
56169
+ var MAX_CURSOR_BYTES = 4096;
56170
+ var PROCESS_CURSOR_SECRET = randomBytes(32).toString("base64url");
56171
+ var CURSOR_MAC_DOMAIN = "mementos.audit.cursor.v1\x00";
56172
+ function protocol(operation, error) {
56173
+ if (error instanceof MementosApiProtocolError)
56174
+ throw error;
56175
+ const detail = error instanceof Error ? error.message : String(error);
56176
+ throw new MementosApiProtocolError(operation, detail);
56177
+ }
56178
+ function safeLimit(value, fallback) {
56179
+ const resolved = value ?? fallback;
56180
+ if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > MAX_AUDIT_LIMIT) {
56181
+ throw new AuditContractError(`limit must be an integer between 1 and ${MAX_AUDIT_LIMIT}`);
56182
+ }
56183
+ return resolved;
56184
+ }
56185
+ function printableIdentifier(value, label, maximum = 512) {
56186
+ if (value === undefined)
56187
+ return null;
56188
+ if (!value || value.length > maximum || /[\u0000-\u001f\u007f]/.test(value)) {
56189
+ throw new AuditContractError(`${label} must be a non-empty printable string of at most ${maximum} characters`);
56190
+ }
56191
+ return value;
56192
+ }
56193
+ function normalizeAuditFilters(input) {
56194
+ const filters = {
56195
+ memory_id: printableIdentifier(input.memory_id, "memory_id"),
56196
+ since: input.since === undefined ? null : canonicalAuditTimestamp(input.since, "since"),
56197
+ until: input.until === undefined ? null : canonicalAuditTimestamp(input.until, "until"),
56198
+ operation: input.operation === undefined ? null : auditOperation(input.operation),
56199
+ agent_id: printableIdentifier(input.agent_id, "agent_id")
56200
+ };
56201
+ if (filters.since && filters.until && filters.since > filters.until) {
56202
+ throw new AuditContractError("since must not be after until");
56203
+ }
56204
+ return filters;
56205
+ }
56206
+ function filterFingerprint(filters) {
56207
+ return createHash2("sha256").update(JSON.stringify(filters), "utf8").digest("hex");
56208
+ }
56209
+ function auditCursorSecret() {
56210
+ return process.env["MEMENTOS_AUDIT_CURSOR_SECRET"]?.trim() || process.env["API_KEY_SIGNING_SECRET"]?.trim() || process.env["HASNA_MEMENTOS_API_SIGNING_KEY"]?.trim() || process.env["HASNA_API_SIGNING_KEY"]?.trim() || PROCESS_CURSOR_SECRET;
56211
+ }
56212
+ function cursorMac(payload, secret) {
56213
+ return createHmac("sha256", secret).update(CURSOR_MAC_DOMAIN, "utf8").update(JSON.stringify(payload), "utf8").digest();
56214
+ }
56215
+ function encodeAuditCursor(payload, secret) {
56216
+ const mac = cursorMac(payload, secret).toString("base64url");
56217
+ return Buffer.from(JSON.stringify({ ...payload, mac }), "utf8").toString("base64url");
56218
+ }
56219
+ function decodeAuditCursor(cursor, contract, filters, secret) {
56220
+ if (!cursor || Buffer.byteLength(cursor, "utf8") > MAX_CURSOR_BYTES || !/^[A-Za-z0-9_-]+$/.test(cursor)) {
56221
+ throw new AuditContractError("cursor is not a valid bounded audit cursor");
56222
+ }
56223
+ let raw;
56224
+ try {
56225
+ raw = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
56226
+ } catch {
56227
+ throw new AuditContractError("cursor is not valid base64url JSON");
56228
+ }
56229
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
56230
+ throw new AuditContractError("cursor payload must be an object");
56231
+ const object2 = raw;
56232
+ const keys = Object.keys(object2).sort();
56233
+ const expectedKeys = [
56234
+ "after_created_at",
56235
+ "after_id",
56236
+ "consumed",
56237
+ "contract",
56238
+ "filter_fingerprint",
56239
+ "mac",
56240
+ "snapshot_created_at",
56241
+ "snapshot_id",
56242
+ "snapshot_total",
56243
+ "v"
56244
+ ].sort();
56245
+ if (JSON.stringify(keys) !== JSON.stringify(expectedKeys))
56246
+ throw new AuditContractError("cursor payload fields are invalid");
56247
+ const timestamp = (value, label) => value === null ? null : canonicalAuditTimestamp(value, label);
56248
+ const identifier = (value, label) => {
56249
+ if (value === null)
56250
+ return null;
56251
+ if (typeof value !== "string")
56252
+ throw new AuditContractError(`${label} must be a string or null`);
56253
+ return printableIdentifier(value, label);
56254
+ };
56255
+ const payload = {
56256
+ v: object2.v === AUDIT_CURSOR_VERSION ? AUDIT_CURSOR_VERSION : (() => {
56257
+ throw new AuditContractError("unsupported cursor version");
56258
+ })(),
56259
+ contract: object2.contract === AUDIT_TRAIL_CONTRACT || object2.contract === AUDIT_EXPORT_CONTRACT ? object2.contract : (() => {
56260
+ throw new AuditContractError("cursor contract is invalid");
56261
+ })(),
56262
+ snapshot_created_at: timestamp(object2.snapshot_created_at, "cursor.snapshot_created_at"),
56263
+ snapshot_id: identifier(object2.snapshot_id, "cursor.snapshot_id"),
56264
+ after_created_at: timestamp(object2.after_created_at, "cursor.after_created_at"),
56265
+ after_id: identifier(object2.after_id, "cursor.after_id"),
56266
+ snapshot_total: Number.isSafeInteger(object2.snapshot_total) && object2.snapshot_total >= 0 ? object2.snapshot_total : (() => {
56267
+ throw new AuditContractError("cursor.snapshot_total must be a non-negative safe integer");
56268
+ })(),
56269
+ consumed: Number.isSafeInteger(object2.consumed) && object2.consumed >= 0 ? object2.consumed : (() => {
56270
+ throw new AuditContractError("cursor.consumed must be a non-negative safe integer");
56271
+ })(),
56272
+ filter_fingerprint: typeof object2.filter_fingerprint === "string" ? object2.filter_fingerprint : ""
56273
+ };
56274
+ const encodedMac = object2.mac;
56275
+ if (typeof encodedMac !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(encodedMac)) {
56276
+ throw new AuditContractError("cursor authentication code is invalid");
56277
+ }
56278
+ const providedMac = Buffer.from(encodedMac, "base64url");
56279
+ const expectedMac = cursorMac(payload, secret);
56280
+ if (providedMac.length !== expectedMac.length || !timingSafeEqual(providedMac, expectedMac) || payload.contract !== contract || payload.filter_fingerprint !== filterFingerprint(filters)) {
56281
+ throw new AuditContractError("cursor authentication or query binding failed");
56282
+ }
56283
+ if (!payload.snapshot_created_at || !payload.snapshot_id || !payload.after_created_at || !payload.after_id) {
56284
+ throw new AuditContractError("continuation cursor is missing its snapshot or ordering boundary");
56285
+ }
56286
+ if (payload.after_created_at > payload.snapshot_created_at || payload.after_created_at === payload.snapshot_created_at && payload.after_id > payload.snapshot_id) {
56287
+ throw new AuditContractError("cursor continuation lies outside its snapshot boundary");
56288
+ }
56289
+ return payload;
56290
+ }
56291
+ function dbTimestamp(value, db) {
56292
+ if (db instanceof PgAdapter)
56293
+ return new Date(value);
56294
+ const withoutZone = value.slice(0, -1).replace("T", " ");
56295
+ return withoutZone.endsWith(".000") ? withoutZone.slice(0, -4) : withoutZone;
56296
+ }
56297
+ function normalizedDbEntry(row) {
56298
+ let changes = row.changes;
56299
+ if (typeof changes === "string") {
56300
+ try {
56301
+ changes = JSON.parse(changes || "{}");
56302
+ } catch {
56303
+ throw new AuditContractError("audit database row changes is not valid JSON");
56304
+ }
56305
+ }
56306
+ return validateAuditEntry({
56307
+ id: row.id,
56308
+ memory_id: row.memory_id,
56309
+ memory_key: row.memory_key ?? null,
56310
+ operation: row.operation,
56311
+ agent_id: row.agent_id ?? null,
56312
+ old_value_hash: row.old_value_hash ?? null,
56313
+ new_value_hash: row.new_value_hash ?? null,
56314
+ changes: changes ?? {},
56315
+ created_at: canonicalAuditTimestamp(row.created_at, "audit database row created_at")
56316
+ }, "audit database row");
56317
+ }
56318
+ function buildFilterSql(filters, db) {
56319
+ const conditions = [];
56320
+ const params = [];
56321
+ if (filters.memory_id) {
56322
+ conditions.push("memory_id = ?");
56323
+ params.push(filters.memory_id);
56324
+ }
56325
+ if (filters.since) {
56326
+ conditions.push("created_at >= ?");
56327
+ params.push(dbTimestamp(filters.since, db));
56328
+ }
56329
+ if (filters.until) {
56330
+ conditions.push("created_at <= ?");
56331
+ params.push(dbTimestamp(filters.until, db));
56332
+ }
56333
+ if (filters.operation) {
56334
+ conditions.push("operation = ?");
56335
+ params.push(filters.operation);
56336
+ }
56337
+ if (filters.agent_id) {
56338
+ conditions.push("agent_id = ?");
56339
+ params.push(filters.agent_id);
56340
+ }
56341
+ return { conditions, params };
56342
+ }
56343
+ function auditSortExpression(db) {
56344
+ return db instanceof PgAdapter ? "date_trunc('milliseconds', created_at)" : "created_at";
56345
+ }
56346
+ function boundarySql(payload, db) {
56347
+ const conditions = [];
56348
+ const params = [];
56349
+ const createdAt = auditSortExpression(db);
56350
+ if (payload.snapshot_created_at && payload.snapshot_id) {
56351
+ const value = dbTimestamp(payload.snapshot_created_at, db);
56352
+ conditions.push(`(${createdAt} < ? OR (${createdAt} = ? AND id <= ?))`);
56353
+ params.push(value, value, payload.snapshot_id);
56354
+ }
56355
+ if (payload.after_created_at && payload.after_id) {
56356
+ const value = dbTimestamp(payload.after_created_at, db);
56357
+ conditions.push(`(${createdAt} < ? OR (${createdAt} = ? AND id < ?))`);
56358
+ params.push(value, value, payload.after_id);
56359
+ }
56360
+ return { conditions, params };
56361
+ }
56362
+ function whereClause(conditions) {
56363
+ return conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
56364
+ }
56365
+ function numberFromDb(value, label) {
56366
+ const number = typeof value === "number" ? value : Number(value);
56367
+ if (!Number.isSafeInteger(number) || number < 0)
56368
+ throw new AuditContractError(`${label} is not a non-negative safe integer`);
56369
+ return number;
56370
+ }
56371
+ function readAuditPage(contract, filters, options, db) {
56372
+ const limit = safeLimit(options.limit, contract === AUDIT_TRAIL_CONTRACT ? DEFAULT_TRAIL_LIMIT : DEFAULT_EXPORT_LIMIT);
56373
+ const requestedCursor = options.cursor ?? null;
56374
+ const secret = auditCursorSecret();
56375
+ const continuation = requestedCursor ? decodeAuditCursor(requestedCursor, contract, filters, secret) : null;
56376
+ return db.transaction(() => {
56377
+ const base = buildFilterSql(filters, db);
56378
+ let snapshotCreatedAt = continuation?.snapshot_created_at ?? null;
56379
+ let snapshotId = continuation?.snapshot_id ?? null;
56380
+ if (!continuation) {
56381
+ const createdAt = auditSortExpression(db);
56382
+ const snapshotRow = db.query(`SELECT ${createdAt} AS created_at, id FROM memory_audit_log ${whereClause(base.conditions)} ORDER BY ${createdAt} DESC, id DESC LIMIT 1`).get(...base.params);
56383
+ if (snapshotRow) {
56384
+ snapshotCreatedAt = canonicalAuditTimestamp(snapshotRow.created_at, "audit snapshot created_at");
56385
+ snapshotId = printableIdentifier(String(snapshotRow.id ?? ""), "audit snapshot id");
56386
+ }
56387
+ }
56388
+ const snapshotBoundary = boundarySql({
56389
+ snapshot_created_at: snapshotCreatedAt,
56390
+ snapshot_id: snapshotId,
56391
+ after_created_at: null,
56392
+ after_id: null
56393
+ }, db);
56394
+ const countConditions = [...base.conditions, ...snapshotBoundary.conditions];
56395
+ const countParams = [...base.params, ...snapshotBoundary.params];
56396
+ const totalRow = db.query(`SELECT COUNT(*) AS count FROM memory_audit_log ${whereClause(countConditions)}`).get(...countParams);
56397
+ const total = numberFromDb(totalRow?.count ?? 0, "audit total");
56398
+ if (continuation && continuation.snapshot_total !== total) {
56399
+ throw new AuditContractError("audit snapshot changed while following the cursor; restart from the first page");
56400
+ }
56401
+ const pageBoundary = boundarySql({
56402
+ snapshot_created_at: snapshotCreatedAt,
56403
+ snapshot_id: snapshotId,
56404
+ after_created_at: continuation?.after_created_at ?? null,
56405
+ after_id: continuation?.after_id ?? null
56406
+ }, db);
56407
+ const pageConditions = [...base.conditions, ...pageBoundary.conditions];
56408
+ const pageParams = [...base.params, ...pageBoundary.params];
56409
+ const rawRows = db.query(`SELECT id, memory_id, memory_key, operation, agent_id, old_value_hash, new_value_hash, changes, created_at
56410
+ FROM memory_audit_log ${whereClause(pageConditions)}
56411
+ ORDER BY ${auditSortExpression(db)} DESC, id DESC LIMIT ?`).all(...pageParams, limit + 1);
56412
+ const hasExtra = rawRows.length > limit;
56413
+ const rows = hasExtra ? rawRows.slice(0, limit) : rawRows;
56414
+ const entries = rows.map(normalizedDbEntry);
56415
+ const consumedBefore = continuation?.consumed ?? 0;
56416
+ const consumed = consumedBefore + entries.length;
56417
+ if (consumed > total || hasExtra !== consumed < total) {
56418
+ throw new AuditContractError("audit page changed inside its database snapshot");
56419
+ }
56420
+ const hasMore = consumed < total;
56421
+ const last = entries.at(-1);
56422
+ const nextCursor = hasMore && last && snapshotCreatedAt && snapshotId ? encodeAuditCursor({
56423
+ v: AUDIT_CURSOR_VERSION,
56424
+ contract,
56425
+ snapshot_created_at: snapshotCreatedAt,
56426
+ snapshot_id: snapshotId,
56427
+ after_created_at: last.created_at,
56428
+ after_id: last.id,
56429
+ snapshot_total: total,
56430
+ consumed,
56431
+ filter_fingerprint: filterFingerprint(filters)
56432
+ }, secret) : null;
56433
+ const page = {
56434
+ contract,
56435
+ entries,
56436
+ count: entries.length,
56437
+ total,
56438
+ limit,
56439
+ cursor: requestedCursor,
56440
+ next_cursor: nextCursor,
56441
+ consumed,
56442
+ has_more: hasMore,
56443
+ complete: requestedCursor === null && !hasMore,
56444
+ snapshot_at: snapshotCreatedAt ?? now(),
56445
+ filters,
56446
+ sort: { field: "created_at", direction: "desc", tie_breaker: "id" }
56447
+ };
56448
+ return validateAuditPage(page, { contract, cursor: requestedCursor, filters, limit });
56449
+ });
56450
+ }
56451
+ function hostedAuditPage(contract, path, filters, options) {
56452
+ const limit = safeLimit(options.limit, contract === AUDIT_TRAIL_CONTRACT ? DEFAULT_TRAIL_LIMIT : DEFAULT_EXPORT_LIMIT);
56453
+ const cursor = options.cursor ?? null;
56454
+ const operation = `GET ${path.split("?")[0]}`;
56455
+ const { data } = apiJson("GET", path);
56456
+ try {
56457
+ return validateAuditPage(data, { contract, cursor, filters, limit });
56458
+ } catch (error) {
56459
+ return protocol(operation, error);
56460
+ }
56461
+ }
56462
+ function getMemoryAuditTrailPage(memoryId, options = {}, db) {
56463
+ const filters = normalizeAuditFilters({ memory_id: memoryId });
56464
+ const limit = safeLimit(options.limit, DEFAULT_TRAIL_LIMIT);
56465
+ if (!db && isApiMode()) {
56466
+ const query = toQuery({ limit, cursor: options.cursor });
56467
+ return hostedAuditPage(AUDIT_TRAIL_CONTRACT, `/memories/${encodeURIComponent(filters.memory_id)}/audit-trail${query}`, filters, { limit, cursor: options.cursor });
56468
+ }
56469
+ return readAuditPage(AUDIT_TRAIL_CONTRACT, filters, { limit, cursor: options.cursor }, db ?? getDatabase());
56470
+ }
56471
+ function exportAuditLogPage(options = {}, db) {
56472
+ const filters = normalizeAuditFilters(options);
56473
+ const limit = safeLimit(options.limit, DEFAULT_EXPORT_LIMIT);
56474
+ if (!db && isApiMode()) {
56475
+ const query = toQuery({
56476
+ since: filters.since ?? undefined,
56477
+ until: filters.until ?? undefined,
56478
+ operation: filters.operation ?? undefined,
56479
+ agent_id: filters.agent_id ?? undefined,
56480
+ limit,
56481
+ cursor: options.cursor
56482
+ });
56483
+ return hostedAuditPage(AUDIT_EXPORT_CONTRACT, `/audit/export${query}`, filters, { limit, cursor: options.cursor });
56484
+ }
56485
+ return readAuditPage(AUDIT_EXPORT_CONTRACT, filters, { limit, cursor: options.cursor }, db ?? getDatabase());
56486
+ }
56487
+ function getMemoryAuditTrail(memoryId, limit = DEFAULT_TRAIL_LIMIT, db) {
56488
+ return getMemoryAuditTrailPage(memoryId, { limit }, db).entries;
56489
+ }
56490
+ function exportAuditLog(options = {}, db) {
56491
+ return exportAuditLogPage(options, db).entries;
56492
+ }
56493
+ function auditStatsFromDb(db) {
56494
+ const snapshotAt = now();
56495
+ const recentSince = new Date(new Date(snapshotAt).getTime() - 24 * 60 * 60 * 1000).toISOString();
56496
+ const snapshotParam = dbTimestamp(snapshotAt, db);
56497
+ const recentParam = dbTimestamp(recentSince, db);
56498
+ const row = db.query(`SELECT
56499
+ COUNT(*) AS total_entries,
56500
+ SUM(CASE WHEN operation = 'create' THEN 1 ELSE 0 END) AS create_count,
56501
+ SUM(CASE WHEN operation = 'update' THEN 1 ELSE 0 END) AS update_count,
56502
+ SUM(CASE WHEN operation = 'delete' THEN 1 ELSE 0 END) AS delete_count,
56503
+ SUM(CASE WHEN operation = 'archive' THEN 1 ELSE 0 END) AS archive_count,
56504
+ SUM(CASE WHEN operation = 'restore' THEN 1 ELSE 0 END) AS restore_count,
56505
+ SUM(CASE WHEN operation = 'read' THEN 1 ELSE 0 END) AS read_count,
56506
+ SUM(CASE WHEN created_at >= ? AND created_at <= ? THEN 1 ELSE 0 END) AS recent_24h
56507
+ FROM memory_audit_log
56508
+ WHERE created_at <= ?`).get(recentParam, snapshotParam, snapshotParam);
56509
+ const byOperation = Object.fromEntries(AUDIT_OPERATIONS.map((operation) => [
56510
+ operation,
56511
+ numberFromDb(row?.[`${operation}_count`] ?? 0, `audit ${operation} count`)
56512
+ ]));
56513
+ return validateAuditStats({
56514
+ contract: AUDIT_STATS_CONTRACT,
56515
+ total_entries: numberFromDb(row?.total_entries ?? 0, "audit total_entries"),
56516
+ by_operation: byOperation,
56517
+ recent_24h: numberFromDb(row?.recent_24h ?? 0, "audit recent_24h"),
56518
+ snapshot_at: snapshotAt
56519
+ });
56520
+ }
56521
+ function getAuditStats(db) {
56522
+ if (!db && isApiMode()) {
56523
+ const operation = "GET /audit/stats";
56524
+ const { data } = apiJson("GET", "/audit/stats");
56525
+ try {
56526
+ return validateAuditStats(data);
56527
+ } catch (error) {
56528
+ return protocol(operation, error);
56529
+ }
56530
+ }
56531
+ return auditStatsFromDb(db ?? getDatabase());
56532
+ }
56533
+ // src/lib/memory-lock.ts
55559
56534
  var MEMORY_WRITE_TTL = 30;
55560
56535
  function memoryLockId(key, scope, projectId) {
55561
56536
  return `${scope}:${key}:${projectId ?? ""}`;
55562
56537
  }
55563
56538
  function acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds = MEMORY_WRITE_TTL, db) {
55564
- const d = db || getDatabase();
55565
- return acquireLock(agentId, "memory", memoryLockId(key, scope, projectId), "exclusive", ttlSeconds, d);
56539
+ return acquireLock(agentId, "memory", memoryLockId(key, scope, projectId), "exclusive", ttlSeconds, db);
55566
56540
  }
55567
56541
  function releaseMemoryWriteLock(lockId, agentId, db) {
55568
- const d = db || getDatabase();
55569
- return releaseLock(lockId, agentId, d);
56542
+ return releaseLock(lockId, agentId, db);
55570
56543
  }
55571
56544
  function checkMemoryWriteLock(key, scope, projectId, db) {
55572
- const d = db || getDatabase();
55573
- const locks = checkLock("memory", memoryLockId(key, scope, projectId), "exclusive", d);
56545
+ const locks = checkLock("memory", memoryLockId(key, scope, projectId), "exclusive", db);
55574
56546
  return locks[0] ?? null;
55575
56547
  }
55576
56548
  function withMemoryLock(agentId, key, scope, projectId, fn, ttlSeconds = MEMORY_WRITE_TTL, db) {
55577
- const d = db || getDatabase();
55578
- const lock = acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds, d);
56549
+ const lock = acquireMemoryWriteLock(agentId, key, scope, projectId, ttlSeconds, db);
55579
56550
  if (!lock) {
55580
- const existing = checkMemoryWriteLock(key, scope, projectId, d);
56551
+ const existing = checkMemoryWriteLock(key, scope, projectId, db);
55581
56552
  throw new MemoryLockConflictError(key, scope, existing?.agent_id ?? "unknown");
55582
56553
  }
55583
56554
  try {
55584
56555
  return fn();
55585
56556
  } finally {
55586
- releaseLock(lock.id, agentId, d);
56557
+ releaseLock(lock.id, agentId, db);
55587
56558
  }
55588
56559
  }
55589
56560
 
@@ -55667,7 +56638,7 @@ function focusFilterSQL(agentId, projectId) {
55667
56638
  // src/db/projects.ts
55668
56639
  init_database();
55669
56640
  init_api_mode();
55670
- import { createHash as createHash2 } from "crypto";
56641
+ import { createHash as createHash3 } from "crypto";
55671
56642
  function parseProjectRow2(row) {
55672
56643
  return {
55673
56644
  id: row["id"],
@@ -55729,7 +56700,7 @@ function canonicalProjectUpdateJson(value) {
55729
56700
  return JSON.stringify(canonicalizeProjectUpdateValue(value));
55730
56701
  }
55731
56702
  function digestProjectUpdateValue(value) {
55732
- return createHash2("sha256").update(canonicalProjectUpdateJson(value)).digest("hex");
56703
+ return createHash3("sha256").update(canonicalProjectUpdateJson(value)).digest("hex");
55733
56704
  }
55734
56705
  function timestampString2(value) {
55735
56706
  return value instanceof Date ? value.toISOString() : String(value);
@@ -56152,7 +57123,7 @@ function getProjectUpdateReceipt(id, receiptId, identity = projectUpdateAuthorit
56152
57123
  return receipt;
56153
57124
  }
56154
57125
  // src/project-registration/authority.ts
56155
- import { createHash as createHash3 } from "crypto";
57126
+ import { createHash as createHash4 } from "crypto";
56156
57127
  import { resolve as resolve3 } from "path";
56157
57128
 
56158
57129
  // src/project-registration/project-references.ts
@@ -56243,10 +57214,10 @@ function canonicalize2(value) {
56243
57214
  return out;
56244
57215
  }
56245
57216
  function digestMementosProjectRegistrationValue(value) {
56246
- return createHash3("sha256").update(canonicalMementosProjectRegistrationJson(value)).digest("hex");
57217
+ return createHash4("sha256").update(canonicalMementosProjectRegistrationJson(value)).digest("hex");
56247
57218
  }
56248
57219
  function digestOwnedPath(path) {
56249
- return createHash3("sha256").update(path).digest("hex");
57220
+ return createHash4("sha256").update(path).digest("hex");
56250
57221
  }
56251
57222
  function deriveMementosProjectRegistrationIdempotencyKey(input) {
56252
57223
  return `prk_${digestMementosProjectRegistrationValue({
@@ -56317,7 +57288,7 @@ function requireString(value, field, options = {}) {
56317
57288
  }
56318
57289
  return value;
56319
57290
  }
56320
- function exactKeys(value, expected, field) {
57291
+ function exactKeys2(value, expected, field) {
56321
57292
  const actual = Object.keys(value).sort();
56322
57293
  const wanted = [...expected].sort();
56323
57294
  if (actual.length !== wanted.length || actual.some((key, i) => key !== wanted[i])) {
@@ -56374,7 +57345,7 @@ function assertGuardedUpdateRequest(targetId, request, capability) {
56374
57345
  if (!request.updates || typeof request.updates !== "object") {
56375
57346
  throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "guarded project updates must contain exactly one private path handle");
56376
57347
  }
56377
- exactKeys(request.updates, ["path"], "updates");
57348
+ exactKeys2(request.updates, ["path"], "updates");
56378
57349
  return { identity, path: ownedPath(request.updates.path) };
56379
57350
  }
56380
57351
  function publicGuardedUpdateReceipt(receipt, capability) {
@@ -56698,7 +57669,7 @@ function assertForwardRequest2(request, capability) {
56698
57669
  if (request.direction !== "forward" || request.accepted_receipt !== undefined) {
56699
57670
  throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "conditional create requires a forward request without an accepted receipt");
56700
57671
  }
56701
- exactKeys(request.desired, [
57672
+ exactKeys2(request.desired, [
56702
57673
  "name",
56703
57674
  "source_project_id",
56704
57675
  "source_project_slug",
@@ -56732,7 +57703,7 @@ function assertInverseRequest(request, capability) {
56732
57703
  if (accepted.authority !== "mementos" || accepted.route !== capability.route || accepted.package_version !== capability.package_version || accepted.authority_id !== capability.authority_id || accepted.tenant_id !== capability.tenant_id || accepted.corpus_id !== capability.corpus_id || accepted.operation_id !== request.operation_id || accepted.step_id !== request.step_id || accepted.resource_kind !== "project" || accepted.direction !== "forward" || accepted.outcome !== "accepted" || !accepted.created_by_operation || !accepted.target_id || !accepted.result_revision || !accepted.result_digest || request.target_selector !== accepted.target_id) {
56733
57704
  throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "accepted receipt is not an exact attempt-created forward receipt");
56734
57705
  }
56735
- exactKeys(request.desired, ["accepted_receipt_id", "target_id"], "inverse desired");
57706
+ exactKeys2(request.desired, ["accepted_receipt_id", "target_id"], "inverse desired");
56736
57707
  if (request.desired["accepted_receipt_id"] !== accepted.receipt_id || request.desired["target_id"] !== accepted.target_id) {
56737
57708
  throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "inverse desired payload does not bind the accepted receipt");
56738
57709
  }
@@ -56826,8 +57797,8 @@ class PackageOwnedMementosProjectRegistrationAuthority {
56826
57797
  target_id: accepted.target_id
56827
57798
  });
56828
57799
  }
56829
- const record = projectRecord(this.db, project);
56830
- if (record.revision !== accepted.result_revision || record.digest !== accepted.result_digest) {
57800
+ const record2 = projectRecord(this.db, project);
57801
+ if (record2.revision !== accepted.result_revision || record2.digest !== accepted.result_digest) {
56831
57802
  return this.terminal(request, callDigest, "accepted_target_drifted", {
56832
57803
  target_id: accepted.target_id
56833
57804
  });
@@ -56854,7 +57825,7 @@ class PackageOwnedMementosProjectRegistrationAuthority {
56854
57825
  if (project.updated_at !== receipt.result_revision) {
56855
57826
  throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE", "guarded project result revision did not match its immutable receipt");
56856
57827
  }
56857
- const record = {
57828
+ const record2 = {
56858
57829
  target_id: targetId2,
56859
57830
  revision: receipt.result_revision,
56860
57831
  digest: receipt.result_digest
@@ -56862,7 +57833,7 @@ class PackageOwnedMementosProjectRegistrationAuthority {
56862
57833
  return withBoundedResponseControl({
56863
57834
  dry_run: false,
56864
57835
  applied: true,
56865
- record,
57836
+ record: record2,
56866
57837
  receipt: publicGuardedUpdateReceipt(receipt, this.capabilityValue)
56867
57838
  }, bounds, startedAt);
56868
57839
  }
@@ -56991,12 +57962,12 @@ class PackageOwnedMementosProjectRegistrationAuthority {
56991
57962
  if (!project || project.path !== path) {
56992
57963
  throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_CONFLICT", "project write did not read back by its full id");
56993
57964
  }
56994
- const record = projectRecord(this.db, project);
57965
+ const record2 = projectRecord(this.db, project);
56995
57966
  const acceptedReceipt = makeReceipt2(request, this.capabilityValue, callDigest, now2, {
56996
57967
  outcome: "accepted",
56997
- target_id: record.target_id,
56998
- result_revision: record.revision,
56999
- result_digest: record.digest,
57968
+ target_id: record2.target_id,
57969
+ result_revision: record2.revision,
57970
+ result_digest: record2.digest,
57000
57971
  created_by_operation: true
57001
57972
  });
57002
57973
  this.writeBoundary("before_receipt_write", request);
@@ -57036,9 +58007,9 @@ class PackageOwnedMementosProjectRegistrationAuthority {
57036
58007
  if (!project || project.path !== path) {
57037
58008
  throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "exact full-id project readback did not resolve the owned path");
57038
58009
  }
57039
- const record = projectRecord(this.db, project);
57040
- assertWithinBounds(record, request, startedAt);
57041
- return record;
58010
+ const record2 = projectRecord(this.db, project);
58011
+ assertWithinBounds(record2, request, startedAt);
58012
+ return record2;
57042
58013
  }
57043
58014
  async lookupReceipt(request) {
57044
58015
  const startedAt = Date.now();
@@ -57816,103 +58787,401 @@ function getMementosProjectResourceExact(projectId, resourceKind, stableId, db,
57816
58787
  }
57817
58788
  // src/db/machines.ts
57818
58789
  init_database();
58790
+ init_api_mode();
58791
+ import { createHash as createHash5 } from "crypto";
57819
58792
  import { hostname, platform } from "os";
57820
- function parseMachine(row) {
58793
+ var MACHINE_REGISTRATION_CONTRACT = "mementos.machine-registration.v1";
58794
+ var MACHINE_LIST_CONTRACT = "mementos.machines.v1";
58795
+ var MACHINE_MUTATION_CONTRACT = "mementos.machine-mutation.v1";
58796
+ var MACHINE_TOUCH_CONTRACT = "mementos.machine-touch.v1";
58797
+
58798
+ class MachineRegistryError extends Error {
58799
+ code;
58800
+ constructor(code, message) {
58801
+ super(message);
58802
+ this.code = code;
58803
+ this.name = "MachineRegistryError";
58804
+ }
58805
+ }
58806
+ var ISO_UTC_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
58807
+ function canonicalTimestamp(value, operation, field) {
58808
+ const stringValue = value instanceof Date ? value.toISOString() : value;
58809
+ if (typeof stringValue !== "string" || !ISO_UTC_TIMESTAMP.test(stringValue)) {
58810
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a canonical UTC ISO-8601 timestamp`);
58811
+ }
58812
+ const parsed = new Date(stringValue);
58813
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== stringValue) {
58814
+ throw new MementosApiProtocolError(operation, `expected '${field}' to be a real calendar timestamp`);
58815
+ }
58816
+ return stringValue;
58817
+ }
58818
+ function machineFromDb(row, operation = "machine database row") {
57821
58819
  if (!row)
57822
58820
  return null;
58821
+ if (typeof row.id !== "string" || !row.id || typeof row.name !== "string" || !row.name || row.name.length > 128 || /[\u0000-\u001f\u007f]/.test(row.name) || typeof row.hostname !== "string" || !row.hostname || typeof row.platform !== "string" || !row.platform) {
58822
+ throw new Error(`${operation} is missing or violates required machine identity fields`);
58823
+ }
58824
+ const canonicalHostname = normalizeMachineHostname(row.hostname);
58825
+ const canonicalPlatform = normalizeMachinePlatform(row.platform);
58826
+ if (canonicalHostname !== row.hostname || canonicalPlatform !== row.platform) {
58827
+ throw new Error(`${operation} contains noncanonical hostname or platform data`);
58828
+ }
58829
+ if (!["boolean", "number"].includes(typeof row.is_primary)) {
58830
+ throw new Error(`${operation} has an invalid is_primary value`);
58831
+ }
58832
+ const createdValue = row.created_at instanceof Date ? row.created_at : new Date(String(row.created_at ?? ""));
58833
+ const lastSeenValue = row.last_seen_at instanceof Date ? row.last_seen_at : new Date(String(row.last_seen_at ?? ""));
58834
+ if (!Number.isFinite(createdValue.getTime()) || !Number.isFinite(lastSeenValue.getTime())) {
58835
+ throw new Error(`${operation} has an invalid timestamp`);
58836
+ }
58837
+ const createdAt = createdValue.toISOString();
58838
+ const lastSeenAt = lastSeenValue.toISOString();
58839
+ if (lastSeenAt < createdAt)
58840
+ throw new Error(`${operation} has last_seen_at before created_at`);
57823
58841
  return {
57824
- ...row,
57825
- is_primary: Boolean(row.is_primary)
57826
- };
58842
+ id: row.id,
58843
+ name: row.name,
58844
+ hostname: canonicalHostname,
58845
+ platform: canonicalPlatform,
58846
+ is_primary: Boolean(row.is_primary),
58847
+ created_at: createdAt,
58848
+ last_seen_at: lastSeenAt
58849
+ };
58850
+ }
58851
+ function decodeMachine(value, operation) {
58852
+ const object2 = expectObject(value, operation);
58853
+ const name = expectString(object2, "name", operation);
58854
+ if (name.length > 128 || /[\u0000-\u001f\u007f]/.test(name)) {
58855
+ throw new MementosApiProtocolError(operation, "machine.name is outside the public contract");
58856
+ }
58857
+ const rawHost = expectString(object2, "hostname", operation);
58858
+ const rawPlatform = expectString(object2, "platform", operation);
58859
+ let machineHostname;
58860
+ let machinePlatform;
58861
+ try {
58862
+ machineHostname = normalizeMachineHostname(rawHost);
58863
+ machinePlatform = normalizeMachinePlatform(rawPlatform);
58864
+ } catch {
58865
+ throw new MementosApiProtocolError(operation, "machine hostname or platform is not canonical");
58866
+ }
58867
+ if (machineHostname !== rawHost || machinePlatform !== rawPlatform) {
58868
+ throw new MementosApiProtocolError(operation, "machine hostname or platform is not canonical");
58869
+ }
58870
+ const createdAt = canonicalTimestamp(object2["created_at"], operation, "created_at");
58871
+ const lastSeenAt = canonicalTimestamp(object2["last_seen_at"], operation, "last_seen_at");
58872
+ if (lastSeenAt < createdAt) {
58873
+ throw new MementosApiProtocolError(operation, "last_seen_at precedes created_at");
58874
+ }
58875
+ return {
58876
+ id: expectString(object2, "id", operation),
58877
+ name,
58878
+ hostname: machineHostname,
58879
+ platform: machinePlatform,
58880
+ is_primary: expectBoolean(object2, "is_primary", operation),
58881
+ created_at: createdAt,
58882
+ last_seen_at: lastSeenAt
58883
+ };
58884
+ }
58885
+ function expectContract(object2, contract, operation) {
58886
+ if (object2["contract"] !== contract) {
58887
+ throw new MementosApiProtocolError(operation, `expected contract '${contract}'`);
58888
+ }
58889
+ }
58890
+ function decodeRegistration(value, expected) {
58891
+ const operation = "POST /v1/machines";
58892
+ const object2 = expectObject(value, operation);
58893
+ expectContract(object2, MACHINE_REGISTRATION_CONTRACT, operation);
58894
+ const machine = decodeMachine(object2["machine"], operation);
58895
+ const created = expectBoolean(object2, "created", operation);
58896
+ const identity = expectObject(object2["identity"], operation);
58897
+ if (identity["idempotency_key"] !== "normalized_hostname") {
58898
+ throw new MementosApiProtocolError(operation, "expected normalized-hostname identity semantics");
58899
+ }
58900
+ if (expectString(identity, "stable_id", operation) !== machine.id) {
58901
+ throw new MementosApiProtocolError(operation, "identity stable_id does not match machine.id");
58902
+ }
58903
+ const expectedHostname = normalizeMachineHostname(expected.hostname);
58904
+ const expectedPlatform = normalizeMachinePlatform(expected.platform);
58905
+ if (machine.hostname !== expectedHostname || machine.platform !== expectedPlatform) {
58906
+ throw new MementosApiProtocolError(operation, "registration receipt does not match the requested hostname/platform");
58907
+ }
58908
+ if (created && machine.name !== normalizeMachineName(expected.name, expectedHostname)) {
58909
+ throw new MementosApiProtocolError(operation, "created registration receipt does not match the requested name");
58910
+ }
58911
+ return { machine, created };
58912
+ }
58913
+ function decodeMachineList(value) {
58914
+ const operation = "GET /v1/machines";
58915
+ const object2 = expectObject(value, operation);
58916
+ expectContract(object2, MACHINE_LIST_CONTRACT, operation);
58917
+ const values = expectArray(object2["machines"], operation, "machines");
58918
+ const count = expectNonNegativeInteger(object2, "count", operation);
58919
+ if (count !== values.length) {
58920
+ throw new MementosApiProtocolError(operation, "count does not match machines.length");
58921
+ }
58922
+ if (object2["complete"] !== true) {
58923
+ throw new MementosApiProtocolError(operation, "expected complete=true");
58924
+ }
58925
+ const machines = values.map((entry, index) => decodeMachine(entry, `${operation} machines[${index}]`));
58926
+ const ids = new Set;
58927
+ const hosts = new Set;
58928
+ const names = new Set;
58929
+ let primaryCount = 0;
58930
+ for (const machine of machines) {
58931
+ if (ids.has(machine.id))
58932
+ throw new MementosApiProtocolError(operation, `duplicate machine id '${machine.id}'`);
58933
+ if (hosts.has(machine.hostname))
58934
+ throw new MementosApiProtocolError(operation, `duplicate machine hostname '${machine.hostname}'`);
58935
+ if (names.has(machine.name))
58936
+ throw new MementosApiProtocolError(operation, `duplicate machine name '${machine.name}'`);
58937
+ ids.add(machine.id);
58938
+ hosts.add(machine.hostname);
58939
+ names.add(machine.name);
58940
+ if (machine.is_primary)
58941
+ primaryCount += 1;
58942
+ }
58943
+ if (primaryCount > 1)
58944
+ throw new MementosApiProtocolError(operation, "more than one machine is primary");
58945
+ return machines;
58946
+ }
58947
+ function decodeMachineMutation(value, operation, expectedId) {
58948
+ const object2 = expectObject(value, operation);
58949
+ expectContract(object2, MACHINE_MUTATION_CONTRACT, operation);
58950
+ const machine = decodeMachine(object2["machine"], operation);
58951
+ if (machine.id !== expectedId) {
58952
+ throw new MementosApiProtocolError(operation, "returned machine id does not match the requested stable id");
58953
+ }
58954
+ return machine;
58955
+ }
58956
+ function normalizeMachineHostname(host) {
58957
+ const normalized = host.trim().replace(/\.+$/, "").toLowerCase();
58958
+ if (!normalized || normalized.length > 253 || /[\u0000-\u001f\u007f/\\\s]/.test(normalized)) {
58959
+ throw new MachineRegistryError("MACHINE_INVALID_INPUT", "Machine hostname must be a non-empty hostname of at most 253 characters");
58960
+ }
58961
+ return normalized;
58962
+ }
58963
+ function normalizeMachinePlatform(value) {
58964
+ const normalized = value.trim().toLowerCase();
58965
+ if (!normalized || normalized.length > 64 || !/^[a-z0-9._-]+$/.test(normalized)) {
58966
+ throw new MachineRegistryError("MACHINE_INVALID_INPUT", "Machine platform must contain 1-64 lowercase letters, digits, dots, underscores, or hyphens");
58967
+ }
58968
+ return normalized;
58969
+ }
58970
+ function normalizeMachineName(value, fallback) {
58971
+ const normalized = value?.trim() || fallback;
58972
+ if (!normalized || normalized.length > 128 || /[\u0000-\u001f\u007f]/.test(normalized)) {
58973
+ throw new MachineRegistryError("MACHINE_INVALID_INPUT", "Machine name must contain 1-128 printable characters");
58974
+ }
58975
+ return normalized;
57827
58976
  }
57828
- function normalizeHostname(host) {
57829
- return host.replace(/\.(local|lan|home|internal)$/i, "");
58977
+ function getMachineById(id, db) {
58978
+ return machineFromDb(db.query("SELECT * FROM machines WHERE id = ?").get(id));
57830
58979
  }
57831
- function registerMachine(name, db = getDatabase()) {
57832
- const rawHost = hostname();
57833
- const host = normalizeHostname(rawHost);
57834
- const plat = platform();
57835
- const machineName = name?.trim() || host;
57836
- const existing = parseMachine(db.query("SELECT * FROM machines WHERE hostname = ?").get(host));
57837
- if (existing) {
57838
- db.run("UPDATE machines SET last_seen_at = ? WHERE id = ?", [now(), existing.id]);
57839
- return parseMachine(db.query("SELECT * FROM machines WHERE id = ?").get(existing.id));
58980
+ function registerMachineRecord(input, db) {
58981
+ const d = db ?? getDatabase();
58982
+ const host = normalizeMachineHostname(input.hostname);
58983
+ const machineName = normalizeMachineName(input.name, host);
58984
+ const machinePlatform = normalizeMachinePlatform(input.platform);
58985
+ const proposedId = uuid();
58986
+ const seenAt = now();
58987
+ try {
58988
+ const row = d.query(`INSERT INTO machines (id, name, hostname, platform, created_at, last_seen_at)
58989
+ VALUES (?, ?, ?, ?, ?, ?)
58990
+ ON CONFLICT(hostname) DO UPDATE SET
58991
+ platform = excluded.platform,
58992
+ last_seen_at = CASE
58993
+ WHEN machines.last_seen_at > excluded.last_seen_at THEN machines.last_seen_at
58994
+ ELSE excluded.last_seen_at
58995
+ END
58996
+ RETURNING *, id = ? AS registration_created`).get(proposedId, machineName, host, machinePlatform, seenAt, seenAt, proposedId);
58997
+ const machine = machineFromDb(row, "machine registration upsert");
58998
+ if (!machine || typeof row?.registration_created !== "boolean" && typeof row?.registration_created !== "number") {
58999
+ throw new MachineRegistryError("MACHINE_REGISTRATION_CONFLICT", "Machine registration returned no stable identity receipt");
59000
+ }
59001
+ return { machine, created: Boolean(row.registration_created) };
59002
+ } catch (error) {
59003
+ if (error instanceof MachineRegistryError)
59004
+ throw error;
59005
+ const details = error && typeof error === "object" ? error : {};
59006
+ const diagnostic = `${String(details.constraint ?? "")} ${String(details.message ?? "")}`;
59007
+ if (/machines(?:_|\.)name|machines_name_key/i.test(diagnostic)) {
59008
+ throw new MachineRegistryError("MACHINE_NAME_CONFLICT", `Machine name already taken: ${machineName}`);
59009
+ }
59010
+ throw error;
57840
59011
  }
57841
- let finalName = machineName;
57842
- let suffix = 2;
57843
- while (db.query("SELECT id FROM machines WHERE name = ?").get(finalName)) {
57844
- finalName = `${machineName}-${suffix++}`;
59012
+ }
59013
+ function registerMachine(name, db) {
59014
+ const host = normalizeMachineHostname(hostname());
59015
+ const plat = normalizeMachinePlatform(platform());
59016
+ if (!db && isApiMode()) {
59017
+ const input = { name: name?.trim() || undefined, hostname: host, platform: plat };
59018
+ const { data } = apiJson("POST", "/machines", input);
59019
+ return decodeRegistration(data, input).machine;
57845
59020
  }
57846
- const id = uuid();
57847
- db.run("INSERT INTO machines (id, name, hostname, platform) VALUES (?, ?, ?, ?)", [id, finalName, host, plat]);
57848
- return parseMachine(db.query("SELECT * FROM machines WHERE id = ?").get(id));
57849
- }
57850
- function listMachines(db = getDatabase()) {
57851
- const rows = db.query("SELECT * FROM machines ORDER BY is_primary DESC, last_seen_at DESC, created_at ASC").all();
57852
- return rows.map((row) => parseMachine(row));
57853
- }
57854
- function getMachine(id, db = getDatabase()) {
57855
- return parseMachine(db.query("SELECT * FROM machines WHERE id = ? OR name = ?").get(id, id));
57856
- }
57857
- function renameMachine(id, newName, db = getDatabase()) {
57858
- const m = parseMachine(db.query("SELECT * FROM machines WHERE id = ?").get(id));
57859
- if (!m)
57860
- throw new Error(`Machine not found: ${id}`);
57861
- const clash = db.query("SELECT id FROM machines WHERE name = ? AND id != ?").get(newName, id);
57862
- if (clash)
57863
- throw new Error(`Machine name already taken: ${newName}`);
57864
- db.run("UPDATE machines SET name = ?, last_seen_at = ? WHERE id = ?", [newName, now(), id]);
57865
- return parseMachine(db.query("SELECT * FROM machines WHERE id = ?").get(id));
57866
- }
57867
- function getPrimaryMachine(db = getDatabase()) {
57868
- return parseMachine(db.query("SELECT * FROM machines WHERE is_primary = 1 LIMIT 1").get());
57869
- }
57870
- function getPrimaryMachineCandidate(db = getDatabase()) {
57871
- if (getPrimaryMachine(db))
59021
+ return registerMachineRecord({ name, hostname: host, platform: plat }, db).machine;
59022
+ }
59023
+ function listMachines(db) {
59024
+ if (!db && isApiMode()) {
59025
+ const { data } = apiJson("GET", "/machines");
59026
+ return decodeMachineList(data);
59027
+ }
59028
+ const d = db ?? getDatabase();
59029
+ const rows = d.query("SELECT * FROM machines ORDER BY is_primary DESC, last_seen_at DESC, created_at ASC, id ASC").all();
59030
+ return rows.map((row) => machineFromDb(row));
59031
+ }
59032
+ function getMachine(idOrName, db) {
59033
+ if (!db && isApiMode()) {
59034
+ const { status, data } = apiJson("GET", `/machines/${encodeURIComponent(idOrName)}`, undefined, { allow404: true });
59035
+ if (status === 404)
59036
+ return null;
59037
+ return decodeMachineMutation(data, "GET /v1/machines/:id", idOrName);
59038
+ }
59039
+ const d = db ?? getDatabase();
59040
+ return machineFromDb(d.query("SELECT * FROM machines WHERE id = ? OR name = ?").get(idOrName, idOrName));
59041
+ }
59042
+ function renameMachine(id, newName, db) {
59043
+ const normalizedName = normalizeMachineName(newName, "");
59044
+ if (!db && isApiMode()) {
59045
+ const { data } = apiJson("PATCH", `/machines/${encodeURIComponent(id)}`, { name: normalizedName });
59046
+ const machine = decodeMachineMutation(data, "PATCH /v1/machines/:id", id);
59047
+ if (machine.name !== normalizedName) {
59048
+ throw new MementosApiProtocolError("PATCH /v1/machines/:id", "returned name does not match the requested rename");
59049
+ }
59050
+ return machine;
59051
+ }
59052
+ const d = db ?? getDatabase();
59053
+ if (!getMachineById(id, d))
59054
+ throw new MachineRegistryError("MACHINE_NOT_FOUND", `Machine not found: ${id}`);
59055
+ try {
59056
+ const updated = machineFromDb(d.query("UPDATE machines SET name = ? WHERE id = ? RETURNING *").get(normalizedName, id));
59057
+ if (!updated)
59058
+ throw new MachineRegistryError("MACHINE_NOT_FOUND", `Machine not found: ${id}`);
59059
+ return updated;
59060
+ } catch (error) {
59061
+ if (error instanceof MachineRegistryError)
59062
+ throw error;
59063
+ const clash = d.query("SELECT id FROM machines WHERE name = ? AND id != ?").get(normalizedName, id);
59064
+ if (clash)
59065
+ throw new MachineRegistryError("MACHINE_NAME_CONFLICT", `Machine name already taken: ${normalizedName}`);
59066
+ throw error;
59067
+ }
59068
+ }
59069
+ function getPrimaryMachine(db) {
59070
+ if (!db && isApiMode())
59071
+ return listMachines().find((machine) => machine.is_primary) ?? null;
59072
+ const d = db ?? getDatabase();
59073
+ return machineFromDb(d.query("SELECT * FROM machines WHERE is_primary = 1 LIMIT 1").get());
59074
+ }
59075
+ function getPrimaryMachineCandidate(db) {
59076
+ if (!db && isApiMode()) {
59077
+ const machines = listMachines();
59078
+ if (machines.some((machine) => machine.is_primary))
59079
+ return null;
59080
+ return [...machines].sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id))[0] ?? null;
59081
+ }
59082
+ const d = db ?? getDatabase();
59083
+ if (getPrimaryMachine(d))
57872
59084
  return null;
57873
- return parseMachine(db.query("SELECT * FROM machines ORDER BY created_at ASC, id ASC LIMIT 1").get());
59085
+ return machineFromDb(d.query("SELECT * FROM machines ORDER BY created_at ASC, id ASC LIMIT 1").get());
57874
59086
  }
57875
- function setPrimaryMachine(id, db = getDatabase()) {
57876
- const machine = getMachine(id, db);
57877
- if (!machine)
57878
- throw new Error(`Machine not found: ${id}`);
57879
- const updatedAt = now();
57880
- db.run("UPDATE machines SET is_primary = 0, last_seen_at = ? WHERE is_primary = 1 AND id != ?", [updatedAt, machine.id]);
57881
- db.run("UPDATE machines SET is_primary = 1, last_seen_at = ? WHERE id = ?", [updatedAt, machine.id]);
57882
- return getMachine(machine.id, db);
57883
- }
57884
- function deleteMachine(id, db = getDatabase()) {
57885
- const machine = getMachine(id, db);
57886
- if (!machine)
57887
- throw new Error(`Machine not found: ${id}`);
57888
- if (machine.is_primary) {
57889
- throw new Error(`Primary machine cannot be deleted: ${machine.name}`);
59087
+ function setPrimaryMachine(id, db) {
59088
+ if (!db && isApiMode()) {
59089
+ const { data } = apiJson("POST", `/machines/${encodeURIComponent(id)}/primary`);
59090
+ const machine = decodeMachineMutation(data, "POST /v1/machines/:id/primary", id);
59091
+ if (!machine.is_primary) {
59092
+ throw new MementosApiProtocolError("POST /v1/machines/:id/primary", "returned machine is not primary");
59093
+ }
59094
+ return machine;
59095
+ }
59096
+ const d = db ?? getDatabase();
59097
+ return d.transaction(() => {
59098
+ if (!getMachineById(id, d))
59099
+ throw new MachineRegistryError("MACHINE_NOT_FOUND", `Machine not found: ${id}`);
59100
+ d.run("UPDATE machines SET is_primary = 0 WHERE is_primary = 1 AND id != ?", id);
59101
+ const updated = machineFromDb(d.query("UPDATE machines SET is_primary = 1 WHERE id = ? RETURNING *").get(id));
59102
+ if (!updated)
59103
+ throw new MachineRegistryError("MACHINE_NOT_FOUND", `Machine not found: ${id}`);
59104
+ return updated;
59105
+ });
59106
+ }
59107
+ function deleteMachine(id, db) {
59108
+ if (!db && isApiMode()) {
59109
+ const operation = "DELETE /v1/machines/:id";
59110
+ const { data } = apiJson("DELETE", `/machines/${encodeURIComponent(id)}`);
59111
+ const object2 = expectObject(data, operation);
59112
+ expectContract(object2, MACHINE_MUTATION_CONTRACT, operation);
59113
+ if (expectString(object2, "id", operation) !== id || object2["deleted"] !== true) {
59114
+ throw new MementosApiProtocolError(operation, "expected deleted=true for the requested stable id");
59115
+ }
59116
+ if (hostedMachineCache?.id === id)
59117
+ hostedMachineCache = null;
59118
+ return;
57890
59119
  }
57891
- db.run("DELETE FROM machines WHERE id = ?", [machine.id]);
59120
+ const d = db ?? getDatabase();
59121
+ const machine = getMachineById(id, d);
59122
+ if (!machine)
59123
+ throw new MachineRegistryError("MACHINE_NOT_FOUND", `Machine not found: ${id}`);
59124
+ if (machine.is_primary)
59125
+ throw new MachineRegistryError("MACHINE_PRIMARY_DELETE_REFUSED", `Primary machine cannot be deleted: ${machine.name}`);
59126
+ d.run("DELETE FROM machines WHERE id = ?", id);
57892
59127
  }
57893
- function getFallbackSyncTargetMachine(db = getDatabase()) {
59128
+ function getFallbackSyncTargetMachine(db) {
57894
59129
  return getPrimaryMachine(db);
57895
59130
  }
57896
- function getPrimaryMachineStartupWarning(db = getDatabase()) {
59131
+ function getPrimaryMachineStartupWarning(db) {
57897
59132
  if (getPrimaryMachine(db))
57898
59133
  return null;
57899
59134
  const candidate = getPrimaryMachineCandidate(db);
57900
- if (!candidate) {
59135
+ if (!candidate)
57901
59136
  return "No primary machine configured. Fallback sync target is unset because no machines are registered yet.";
57902
- }
57903
59137
  return `No primary machine configured. Fallback sync target is unset. Candidate: ${candidate.name} (${candidate.id.slice(0, 8)} / ${candidate.hostname}). Confirm it with set_primary_machine.`;
57904
59138
  }
57905
- function touchMachine(id, db = getDatabase()) {
57906
- db.run("UPDATE machines SET last_seen_at = ? WHERE id = ?", [now(), id]);
57907
- }
57908
- function getCurrentMachineId(db = getDatabase()) {
57909
- const host = normalizeHostname(hostname());
57910
- const m = db.query("SELECT id FROM machines WHERE hostname = ?").get(host);
57911
- if (m) {
57912
- touchMachine(m.id, db);
57913
- return m.id;
59139
+ function touchMachine(id, db) {
59140
+ if (!db && isApiMode()) {
59141
+ const operation = "POST /v1/machines/:id/touch";
59142
+ const { data } = apiJson("POST", `/machines/${encodeURIComponent(id)}/touch`);
59143
+ const object2 = expectObject(data, operation);
59144
+ expectContract(object2, MACHINE_TOUCH_CONTRACT, operation);
59145
+ if (object2["touched"] !== true || expectString(object2, "id", operation) !== id) {
59146
+ throw new MementosApiProtocolError(operation, "expected touched=true for the requested stable id");
59147
+ }
59148
+ const machine2 = decodeMachine(object2["machine"], operation);
59149
+ const touchedAt2 = canonicalTimestamp(object2["touched_at"], operation, "touched_at");
59150
+ if (machine2.id !== id || machine2.last_seen_at !== touchedAt2) {
59151
+ throw new MementosApiProtocolError(operation, "touch receipt does not match the returned machine");
59152
+ }
59153
+ return machine2;
57914
59154
  }
57915
- return registerMachine(undefined, db).id;
59155
+ const d = db ?? getDatabase();
59156
+ const touchedAt = now();
59157
+ const machine = machineFromDb(d.query("UPDATE machines SET last_seen_at = ? WHERE id = ? RETURNING *").get(touchedAt, id));
59158
+ if (!machine || machine.id !== id)
59159
+ throw new MachineRegistryError("MACHINE_NOT_FOUND", `Machine not found: ${id}`);
59160
+ return machine;
59161
+ }
59162
+ var hostedMachineCache = null;
59163
+ function getCurrentMachineId(db) {
59164
+ if (!db && isApiMode()) {
59165
+ const host = normalizeMachineHostname(hostname());
59166
+ const plat = normalizeMachinePlatform(platform());
59167
+ const configBefore = getApiConfig();
59168
+ if (!configBefore)
59169
+ throw new Error("mementos hosted machine identity requires a resolved API authority");
59170
+ const credentialFingerprint = createHash5("sha256").update(configBefore.apiKey).digest("hex");
59171
+ const key = `${configBefore.baseUrl}\x00${credentialFingerprint}\x00${host}\x00${plat}`;
59172
+ if (hostedMachineCache?.key === key)
59173
+ return hostedMachineCache.id;
59174
+ const id = registerMachine().id;
59175
+ const configAfter = getApiConfig();
59176
+ const fingerprintAfter = configAfter ? createHash5("sha256").update(configAfter.apiKey).digest("hex") : null;
59177
+ if (!configAfter || configAfter.baseUrl !== configBefore.baseUrl || fingerprintAfter !== credentialFingerprint) {
59178
+ throw new Error("MEMENTOS_AUTHORITY_CHANGED: the machine registry authority or credential changed while resolving the stable machine identity");
59179
+ }
59180
+ hostedMachineCache = { key, id };
59181
+ return id;
59182
+ }
59183
+ const d = db ?? getDatabase();
59184
+ return registerMachine(undefined, d).id;
57916
59185
  }
57917
59186
  // src/lib/search.ts
57918
59187
  init_database();
@@ -59177,14 +60446,27 @@ function ensureDir2(dir) {
59177
60446
  mkdirSync3(dir, { recursive: true });
59178
60447
  }
59179
60448
  }
60449
+ // src/lib/injection-project.ts
60450
+ function resolveInjectionProjectId(projectRef, db) {
60451
+ if (projectRef === undefined)
60452
+ return;
60453
+ const project = getProject(projectRef, db);
60454
+ if (!project)
60455
+ throw new Error(`Project not found: ${projectRef}`);
60456
+ return project.id;
60457
+ }
60458
+
59180
60459
  // src/lib/machine-visibility.ts
60460
+ init_api_mode();
59181
60461
  function resolveVisibleMachineId(machineId, db) {
59182
60462
  if (machineId !== undefined) {
59183
60463
  return machineId;
59184
60464
  }
59185
60465
  try {
59186
60466
  return getCurrentMachineId(db);
59187
- } catch {
60467
+ } catch (error) {
60468
+ if (!db && isApiMode())
60469
+ throw error;
59188
60470
  return null;
59189
60471
  }
59190
60472
  }
@@ -59194,6 +60476,10 @@ function visibleToMachineFilter(machineId, db) {
59194
60476
  };
59195
60477
  }
59196
60478
 
60479
+ // src/lib/profile-synthesizer.ts
60480
+ init_api_mode();
60481
+ init_storage();
60482
+
59197
60483
  // src/db/tool-events.ts
59198
60484
  init_database();
59199
60485
  init_api_mode();
@@ -59206,6 +60492,10 @@ class MemoryInjector {
59206
60492
  this.config = config || loadConfig();
59207
60493
  }
59208
60494
  getInjectionContext(options = {}) {
60495
+ const projectId = resolveInjectionProjectId(options.project_id, options.db);
60496
+ return this.getInjectionContextForProject(options, projectId);
60497
+ }
60498
+ getInjectionContextForProject(options, projectId) {
59209
60499
  const maxTokens = options.max_tokens || this.config.injection.max_tokens;
59210
60500
  const minImportance = options.min_importance || this.config.injection.min_importance;
59211
60501
  const categories = options.categories || this.config.injection.categories;
@@ -59221,13 +60511,13 @@ class MemoryInjector {
59221
60511
  limit: 100
59222
60512
  }, db);
59223
60513
  allMemories.push(...globalMems);
59224
- if (options.project_id) {
60514
+ if (projectId) {
59225
60515
  const sharedMems = listMemories({
59226
60516
  scope: "shared",
59227
60517
  category: categories,
59228
60518
  min_importance: minImportance,
59229
60519
  status: "active",
59230
- project_id: options.project_id,
60520
+ project_id: projectId,
59231
60521
  ...visibleToMachineFilter(visibleMachineId),
59232
60522
  limit: 100
59233
60523
  }, db);
@@ -59240,6 +60530,8 @@ class MemoryInjector {
59240
60530
  min_importance: minImportance,
59241
60531
  status: "active",
59242
60532
  agent_id: options.agent_id,
60533
+ project_id: projectId,
60534
+ include_unassigned_project: true,
59243
60535
  ...visibleToMachineFilter(visibleMachineId),
59244
60536
  limit: 100
59245
60537
  }, db);
@@ -59251,7 +60543,7 @@ class MemoryInjector {
59251
60543
  status: "active",
59252
60544
  ...options.session_id ? { session_id: options.session_id } : {},
59253
60545
  ...options.agent_id ? { agent_id: options.agent_id } : {},
59254
- ...options.project_id ? { project_id: options.project_id } : {},
60546
+ ...projectId ? { project_id: projectId } : {},
59255
60547
  ...visibleToMachineFilter(visibleMachineId),
59256
60548
  limit: 100
59257
60549
  }, db);
@@ -59356,6 +60648,7 @@ ${sections.join(`
59356
60648
  if (!options.query) {
59357
60649
  return this.getInjectionContext(options);
59358
60650
  }
60651
+ const projectId = resolveInjectionProjectId(options.project_id, options.db);
59359
60652
  const maxTokens = options.max_tokens || this.config.injection.max_tokens;
59360
60653
  const minImportance = options.min_importance || this.config.injection.min_importance;
59361
60654
  const categories = options.categories || this.config.injection.categories;
@@ -59371,13 +60664,13 @@ ${sections.join(`
59371
60664
  limit: 100
59372
60665
  }, db);
59373
60666
  allMemories.push(...globalMems);
59374
- if (options.project_id) {
60667
+ if (projectId) {
59375
60668
  const sharedMems = listMemories({
59376
60669
  scope: "shared",
59377
60670
  category: categories,
59378
60671
  min_importance: minImportance,
59379
60672
  status: "active",
59380
- project_id: options.project_id,
60673
+ project_id: projectId,
59381
60674
  ...visibleToMachineFilter(visibleMachineId),
59382
60675
  limit: 100
59383
60676
  }, db);
@@ -59390,6 +60683,8 @@ ${sections.join(`
59390
60683
  min_importance: minImportance,
59391
60684
  status: "active",
59392
60685
  agent_id: options.agent_id,
60686
+ project_id: projectId,
60687
+ include_unassigned_project: true,
59393
60688
  ...visibleToMachineFilter(visibleMachineId),
59394
60689
  limit: 100
59395
60690
  }, db);
@@ -59401,7 +60696,7 @@ ${sections.join(`
59401
60696
  status: "active",
59402
60697
  ...options.session_id ? { session_id: options.session_id } : {},
59403
60698
  ...options.agent_id ? { agent_id: options.agent_id } : {},
59404
- ...options.project_id ? { project_id: options.project_id } : {},
60699
+ ...projectId ? { project_id: projectId } : {},
59405
60700
  ...visibleToMachineFilter(visibleMachineId),
59406
60701
  limit: 100
59407
60702
  }, db);
@@ -59427,7 +60722,7 @@ ${sections.join(`
59427
60722
  } catch {}
59428
60723
  }
59429
60724
  if (embeddingMap.size === 0) {
59430
- return this.getInjectionContext(options);
60725
+ return this.getInjectionContextForProject(options, projectId);
59431
60726
  }
59432
60727
  const nowMs = Date.now();
59433
60728
  const oldestMs = Math.min(...unique.map((m) => new Date(m.updated_at).getTime()));
@@ -59685,6 +60980,14 @@ var defaultSyncAgents = ["claude", "codex", "gemini"];
59685
60980
  // src/lib/storage-sync.ts
59686
60981
  init_storage();
59687
60982
  init_database();
60983
+ init_local_opt_in();
60984
+ var STORAGE_SYNC_LOCAL_ONLY_MESSAGE = "REMOTE_COMMAND_UNSUPPORTED: mementos storage push/pull/sync/status operate on the on-box SQLite store, " + "which is opt-in only (" + `${MEMENTOS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 or an explicit ${MEMENTOS_DB_PATH_ENV_KEYS[0]}` + ") and disabled by default. On the hosted route there is no local store to sync \u2014 refusing rather than " + "creating one. Run these verbs where the on-box store deliberately lives (the local opt-in) or in mementos-serve.";
60985
+ function openLocalSyncAdapter() {
60986
+ if (!isServerContext() && !selectsMementosLocalStore(process.env)) {
60987
+ throw new Error(STORAGE_SYNC_LOCAL_ONLY_MESSAGE);
60988
+ }
60989
+ return new SqliteAdapter(getDbPath2());
60990
+ }
59688
60991
  var MEMORY_TABLE = "memories";
59689
60992
  var MEMORY_SYNC_META_TABLE = "_mementos_storage_sync_meta";
59690
60993
  var SOURCE_MACHINE_METADATA_KEY = "last_synced_source_machine";
@@ -60063,7 +61366,7 @@ function resolveTables(local, tables) {
60063
61366
  function withManagedAdapters(options, fn) {
60064
61367
  const localOwned = !options.local;
60065
61368
  const remoteOwned = !options.remote;
60066
- const local = options.local ?? new SqliteAdapter(getDbPath2());
61369
+ const local = options.local ?? openLocalSyncAdapter();
60067
61370
  const remote = options.remote ?? new PgAdapter(getStorageConnectionString("mementos"));
60068
61371
  try {
60069
61372
  const currentMachineId = resolveCurrentMachineId(local, options.current_machine_id);
@@ -60119,7 +61422,7 @@ function pullStorageChanges(options = {}) {
60119
61422
  function getStorageSyncStatus(options = {}) {
60120
61423
  const backend = getStorageBackend();
60121
61424
  const localOwned = !options.local;
60122
- const local = options.local ?? new SqliteAdapter(getDbPath2());
61425
+ const local = options.local ?? openLocalSyncAdapter();
60123
61426
  try {
60124
61427
  const currentMachineId = resolveCurrentMachineId(local, options.current_machine_id);
60125
61428
  return {
@@ -62729,6 +64032,8 @@ export {
62729
64032
  getMemoryLinks,
62730
64033
  getMemoryByKey,
62731
64034
  getMemoryBacklinks,
64035
+ getMemoryAuditTrailPage,
64036
+ getMemoryAuditTrail,
62732
64037
  getMemory,
62733
64038
  getMemoriesForEntity,
62734
64039
  getMemoriesByKey,
@@ -62747,6 +64052,7 @@ export {
62747
64052
  getDatabase,
62748
64053
  getCurrentMachineId,
62749
64054
  getAutoMemoryStats,
64055
+ getAuditStats,
62750
64056
  getAgent,
62751
64057
  getActiveProfile,
62752
64058
  getActiveModel,
@@ -62754,6 +64060,8 @@ export {
62754
64060
  formatMementosProjectPanel,
62755
64061
  focusFilterSQL,
62756
64062
  findPath,
64063
+ exportAuditLogPage,
64064
+ exportAuditLog,
62757
64065
  enforceQuotas,
62758
64066
  digestMementosProjectRegistrationValue,
62759
64067
  deriveMementosProjectRegistrationIdempotencyKey,
@@ -62815,6 +64123,7 @@ export {
62815
64123
  MementosProjectRegistrationHttpClient,
62816
64124
  MementosProjectRegistrationError,
62817
64125
  MementosProjectAuthorityIdentityError,
64126
+ MachineRegistryError,
62818
64127
  MEMORY_PROJECT_LINK_RECEIPT_COLUMNS,
62819
64128
  MEMENTOS_STORAGE_TABLES,
62820
64129
  MEMENTOS_STORAGE_FALLBACK_ENV,
@@ -62828,11 +64137,19 @@ export {
62828
64137
  MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE,
62829
64138
  MEMENTOS_PROJECT_AUTHORITY_ENV,
62830
64139
  MEMENTOS_MEMORY_PROJECT_LINK_ROUTE,
64140
+ MACHINE_TOUCH_CONTRACT,
64141
+ MACHINE_REGISTRATION_CONTRACT,
64142
+ MACHINE_MUTATION_CONTRACT,
64143
+ MACHINE_LIST_CONTRACT,
62831
64144
  InvalidScopeError,
62832
64145
  EntityNotFoundError,
62833
64146
  DuplicateMemoryError,
62834
64147
  DEFAULT_REFLECTION_PROVIDER,
62835
64148
  DEFAULT_REFLECTION_MODEL,
62836
64149
  DEFAULT_MODEL,
62837
- DEFAULT_CONFIG
64150
+ DEFAULT_CONFIG,
64151
+ AUDIT_TRAIL_CONTRACT,
64152
+ AUDIT_STATS_CONTRACT,
64153
+ AUDIT_OPERATIONS,
64154
+ AUDIT_EXPORT_CONTRACT
62838
64155
  };