@hasna/mementos 0.14.88 → 0.16.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 (161) hide show
  1. package/README.md +119 -30
  2. package/bun.lock +4 -65
  3. package/dist/audit-contract.d.ts +87 -0
  4. package/dist/audit-contract.d.ts.map +1 -0
  5. package/dist/cli/commands/info-context.d.ts.map +1 -1
  6. package/dist/cli/commands/info-history.d.ts.map +1 -1
  7. package/dist/cli/commands/info-stale.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-chain.d.ts.map +1 -1
  12. package/dist/cli/commands/memory-cmd-crud.d.ts.map +1 -1
  13. package/dist/cli/commands/memory-cmd-list.d.ts +25 -0
  14. package/dist/cli/commands/memory-cmd-list.d.ts.map +1 -1
  15. package/dist/cli/commands/memory-cmd-recall.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-tail.d.ts.map +1 -1
  18. package/dist/cli/commands/memory-cmd-view.d.ts.map +1 -1
  19. package/dist/cli/commands/memory-cmd-when-to-use.d.ts.map +1 -1
  20. package/dist/cli/commands/project.d.ts.map +1 -1
  21. package/dist/cli/commands/storage.d.ts.map +1 -1
  22. package/dist/cli/commands/system-doctor.d.ts.map +1 -1
  23. package/dist/cli/commands/system-session.d.ts.map +1 -1
  24. package/dist/cli/commands/system-status.d.ts +43 -0
  25. package/dist/cli/commands/system-status.d.ts.map +1 -0
  26. package/dist/cli/commands/system-synthesized-profile.d.ts.map +1 -1
  27. package/dist/cli/commands/system.d.ts.map +1 -1
  28. package/dist/cli/helpers.d.ts.map +1 -1
  29. package/dist/cli/index.js +4964 -2168
  30. package/dist/cli/register-all.d.ts +1 -1
  31. package/dist/cli/register-all.d.ts.map +1 -1
  32. package/dist/db/api-mode.d.ts +145 -37
  33. package/dist/db/api-mode.d.ts.map +1 -1
  34. package/dist/db/api-response-contract.d.ts +23 -0
  35. package/dist/db/api-response-contract.d.ts.map +1 -0
  36. package/dist/db/audit.d.ts +34 -45
  37. package/dist/db/audit.d.ts.map +1 -1
  38. package/dist/db/database.d.ts +1 -0
  39. package/dist/db/database.d.ts.map +1 -1
  40. package/dist/db/locks.d.ts.map +1 -1
  41. package/dist/db/machines.d.ts +50 -13
  42. package/dist/db/machines.d.ts.map +1 -1
  43. package/dist/db/memories.d.ts.map +1 -1
  44. package/dist/db/migrations.d.ts.map +1 -1
  45. package/dist/db/pg-migrate.d.ts.map +1 -1
  46. package/dist/db/pg-migrations.d.ts.map +1 -1
  47. package/dist/db/session-jobs.d.ts +2 -0
  48. package/dist/db/session-jobs.d.ts.map +1 -1
  49. package/dist/db/store-backend.d.ts +8 -2
  50. package/dist/db/store-backend.d.ts.map +1 -1
  51. package/dist/diagnostics/historical-project-registration-receipt.js +299 -50
  52. package/dist/generated/storage-kit/backend.d.ts +12 -2
  53. package/dist/generated/storage-kit/backend.d.ts.map +1 -1
  54. package/dist/generated/storage-kit/index.d.ts +1 -1
  55. package/dist/generated/storage-kit/index.d.ts.map +1 -1
  56. package/dist/generated/storage-kit/migrations.d.ts +21 -0
  57. package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
  58. package/dist/generated/storage-kit/pool.d.ts +2 -5
  59. package/dist/generated/storage-kit/pool.d.ts.map +1 -1
  60. package/dist/generated/storage-kit/query.d.ts +1 -1
  61. package/dist/generated/storage-kit/query.d.ts.map +1 -1
  62. package/dist/index.d.ts +4 -3
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +4328 -1485
  65. package/dist/lib/built-in-hooks.d.ts.map +1 -1
  66. package/dist/lib/config.d.ts.map +1 -1
  67. package/dist/lib/env.d.ts +10 -0
  68. package/dist/lib/env.d.ts.map +1 -0
  69. package/dist/lib/gatherer.d.ts.map +1 -1
  70. package/dist/lib/injection-project.d.ts +10 -0
  71. package/dist/lib/injection-project.d.ts.map +1 -0
  72. package/dist/lib/injector.d.ts +1 -0
  73. package/dist/lib/injector.d.ts.map +1 -1
  74. package/dist/lib/local-opt-in.d.ts +114 -0
  75. package/dist/lib/local-opt-in.d.ts.map +1 -0
  76. package/dist/lib/machine-visibility.d.ts.map +1 -1
  77. package/dist/lib/memory-lock.d.ts.map +1 -1
  78. package/dist/lib/paths.d.ts +9 -0
  79. package/dist/lib/paths.d.ts.map +1 -1
  80. package/dist/lib/profile-synthesizer.d.ts +13 -2
  81. package/dist/lib/profile-synthesizer.d.ts.map +1 -1
  82. package/dist/lib/redact.d.ts +70 -10
  83. package/dist/lib/redact.d.ts.map +1 -1
  84. package/dist/lib/reflection.d.ts.map +1 -1
  85. package/dist/lib/session-queue.d.ts.map +1 -1
  86. package/dist/lib/session-registry.d.ts +38 -1
  87. package/dist/lib/session-registry.d.ts.map +1 -1
  88. package/dist/lib/storage-sync.d.ts +5 -0
  89. package/dist/lib/storage-sync.d.ts.map +1 -1
  90. package/dist/lib/synthesis/index.d.ts.map +1 -1
  91. package/dist/mcp/index.d.ts +1 -1
  92. package/dist/mcp/index.d.ts.map +1 -1
  93. package/dist/mcp/index.js +6215 -2598
  94. package/dist/mcp/profile.d.ts +23 -0
  95. package/dist/mcp/profile.d.ts.map +1 -0
  96. package/dist/mcp/tools/lock-tools.d.ts.map +1 -1
  97. package/dist/mcp/tools/memory-audit.d.ts +1 -1
  98. package/dist/mcp/tools/memory-audit.d.ts.map +1 -1
  99. package/dist/mcp/tools/memory-crud.d.ts.map +1 -1
  100. package/dist/mcp/tools/memory-inject.d.ts.map +1 -1
  101. package/dist/mcp/tools/project-tools.d.ts.map +1 -1
  102. package/dist/mcp/tools/session-tools.d.ts.map +1 -1
  103. package/dist/mcp/tools/tool-registry.d.ts +22 -19
  104. package/dist/mcp/tools/tool-registry.d.ts.map +1 -1
  105. package/dist/mcp/tools/utility-tools.d.ts +2 -1
  106. package/dist/mcp/tools/utility-tools.d.ts.map +1 -1
  107. package/dist/project-registration.js +549 -205
  108. package/dist/sdk/index.d.ts +282 -14
  109. package/dist/sdk/index.d.ts.map +1 -1
  110. package/dist/sdk/index.js +1691 -35
  111. package/dist/server/auth.d.ts +2 -0
  112. package/dist/server/auth.d.ts.map +1 -1
  113. package/dist/server/helpers.d.ts +21 -7
  114. package/dist/server/helpers.d.ts.map +1 -1
  115. package/dist/server/index.d.ts +2 -0
  116. package/dist/server/index.d.ts.map +1 -1
  117. package/dist/server/index.js +6040 -2475
  118. package/dist/server/openapi.d.ts.map +1 -1
  119. package/dist/server/routes/audit.d.ts +2 -0
  120. package/dist/server/routes/audit.d.ts.map +1 -0
  121. package/dist/server/routes/machines.d.ts +2 -0
  122. package/dist/server/routes/machines.d.ts.map +1 -0
  123. package/dist/server/routes/system-sessions.d.ts.map +1 -1
  124. package/dist/server/routes/system-synthesis.d.ts.map +1 -1
  125. package/dist/storage.d.ts.map +1 -1
  126. package/dist/storage.js +78 -129
  127. package/dist/types/index.d.ts +3 -0
  128. package/dist/types/index.d.ts.map +1 -1
  129. package/docker-entrypoint.sh +7 -0
  130. package/hasna.contract.json +19 -7
  131. package/package.json +12 -11
  132. package/postinstall.mjs +52 -1
  133. package/dashboard/bun.lock +0 -519
  134. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts +0 -2
  135. package/dist/cli/__fixtures__/clean-fallback-stub-server.d.ts.map +0 -1
  136. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts +0 -2
  137. package/dist/cli/__fixtures__/io-restore-stub-server.d.ts.map +0 -1
  138. package/dist/cli/brains.d.ts +0 -3
  139. package/dist/cli/brains.d.ts.map +0 -1
  140. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts +0 -2
  141. package/dist/db/__fixtures__/fail-closed-stub-server.d.ts.map +0 -1
  142. package/dist/db/__fixtures__/list-filter-capture-server.d.ts +0 -2
  143. package/dist/db/__fixtures__/list-filter-capture-server.d.ts.map +0 -1
  144. package/dist/db/__fixtures__/list-filter-client-runner.d.ts +0 -2
  145. package/dist/db/__fixtures__/list-filter-client-runner.d.ts.map +0 -1
  146. package/dist/lib/retired-storage-mode.d.ts +0 -11
  147. package/dist/lib/retired-storage-mode.d.ts.map +0 -1
  148. package/dist/test-support/memories-page-stub-server.d.ts +0 -2
  149. package/dist/test-support/memories-page-stub-server.d.ts.map +0 -1
  150. package/dist/test-support/memories-page-stub.d.ts +0 -28
  151. package/dist/test-support/memories-page-stub.d.ts.map +0 -1
  152. package/dist/test-support/pg-sync-stub-worker.d.ts +0 -2
  153. package/dist/test-support/pg-sync-stub-worker.d.ts.map +0 -1
  154. package/dist/test-support/preload-local-store.d.ts +0 -2
  155. package/dist/test-support/preload-local-store.d.ts.map +0 -1
  156. package/dist/test-support/project-authority-identity.d.ts +0 -8
  157. package/dist/test-support/project-authority-identity.d.ts.map +0 -1
  158. package/dist/test-support/store-isolation.d.ts +0 -108
  159. package/dist/test-support/store-isolation.d.ts.map +0 -1
  160. package/dist/test-support/strip-ansi.d.ts +0 -3
  161. package/dist/test-support/strip-ansi.d.ts.map +0 -1
@@ -72,16 +72,7 @@ function serverDataBackendEnvKeys(name) {
72
72
  databaseUrlKeys: [`HASNA_${token}_DATABASE_URL`, `${token}_DATABASE_URL`]
73
73
  };
74
74
  }
75
- function legacyModeKeys(name) {
76
- const token = envToken(name);
77
- return [
78
- `HASNA_${token}_STORAGE_MODE`,
79
- `HASNA_${token}_MODE`,
80
- `${token}_STORAGE_MODE`,
81
- `${token}_MODE`
82
- ];
83
- }
84
- function firstEnv(env, keys) {
75
+ function firstDatabaseUrl(env, keys) {
85
76
  for (const key of keys) {
86
77
  const value = ownString(env, key)?.trim();
87
78
  if (value)
@@ -89,23 +80,8 @@ function firstEnv(env, keys) {
89
80
  }
90
81
  return null;
91
82
  }
92
- function firstDefinedEnvKey(env, keys) {
93
- for (const key of keys) {
94
- if (Object.hasOwn(env, key) && env[key] !== undefined)
95
- return key;
96
- }
97
- return null;
98
- }
99
- function assertNoLegacyStorageMode(name, env = process.env) {
100
- const legacyKey = firstDefinedEnvKey(env, legacyModeKeys(name));
101
- if (!legacyKey)
102
- return;
103
- const canonicalDatabaseUrl = serverDataBackendEnvKeys(name).databaseUrlKeys[0];
104
- throw new Error(`${legacyKey} was removed. Delete the storage-mode variable; ` + `set ${canonicalDatabaseUrl} to select the postgresql server backend, ` + `or leave it unset for sqlite.`);
105
- }
106
83
  function resolveServerDataBackend(name, env = process.env) {
107
- assertNoLegacyStorageMode(name, env);
108
- const databaseUrl = firstEnv(env, serverDataBackendEnvKeys(name).databaseUrlKeys);
84
+ const databaseUrl = firstDatabaseUrl(env, serverDataBackendEnvKeys(name).databaseUrlKeys);
109
85
  if (!databaseUrl) {
110
86
  return {
111
87
  backend: "sqlite",
@@ -122,113 +98,69 @@ function resolveServerDataBackend(name, env = process.env) {
122
98
  };
123
99
  }
124
100
  function resolveDatabaseUrl(name, env = process.env) {
125
- assertNoLegacyStorageMode(name, env);
126
- const hit = firstEnv(env, serverDataBackendEnvKeys(name).databaseUrlKeys);
127
- return hit?.value ?? null;
101
+ return firstDatabaseUrl(env, serverDataBackendEnvKeys(name).databaseUrlKeys)?.value ?? null;
128
102
  }
129
103
  var init_backend = () => {};
130
104
 
131
- // src/lib/retired-storage-mode.ts
132
- function firstDefinedEnvKey2(env, keys) {
133
- for (const key of keys) {
134
- if (Object.hasOwn(env, key) && env[key] !== undefined)
135
- return key;
136
- }
137
- return null;
138
- }
139
- function assertNoLegacyStorageMode2(env = process.env) {
140
- const legacyKey = firstDefinedEnvKey2(env, LEGACY_STORAGE_MODE_KEYS);
141
- if (!legacyKey)
142
- return;
143
- throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client uses the local SQLite store, or the HTTP API selected by ` + `HASNA_MEMENTOS_API_URL + HASNA_MEMENTOS_API_KEY. ` + `On the server, set HASNA_MEMENTOS_DATABASE_URL to select the postgresql backend, ` + `or leave it unset for sqlite.`);
144
- }
145
- var LEGACY_STORAGE_MODE_KEYS;
146
- var init_retired_storage_mode = __esm(() => {
147
- LEGACY_STORAGE_MODE_KEYS = [
148
- "HASNA_MEMENTOS_STORAGE_MODE",
149
- "HASNA_MEMENTOS_MODE",
150
- "MEMENTOS_STORAGE_MODE",
151
- "MEMENTOS_MODE"
152
- ];
153
- });
154
-
155
- // ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
105
+ // src/lib/paths.ts
106
+ import { existsSync } from "fs";
156
107
  import { homedir } from "os";
157
- import { join } from "path";
158
- function assertApp(app) {
108
+ import { join, resolve } from "path";
109
+ import { homedir as pathsResolverHomedir } from "os";
110
+ import { join as pathsResolverJoin } from "path";
111
+ function pathsResolverAssertApp(app) {
159
112
  if (typeof app !== "string" || app.length === 0) {
160
113
  throw new TypeError("paths: app must be a non-empty string");
161
114
  }
162
- if (!APP_SLUG_RE.test(app)) {
115
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
163
116
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
164
117
  }
165
118
  }
166
- function envOf(options) {
167
- return options.env ?? process.env;
168
- }
169
- function envValue(options, kind) {
170
- const value = envOf(options)[KIND_ENV[kind]];
171
- return typeof value === "string" && value.length > 0 ? value : undefined;
172
- }
173
- function isMacOS(platform) {
174
- return platform === "darwin";
119
+ function pathsResolverAssertKind(kind) {
120
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
121
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
122
+ }
175
123
  }
176
- function baseDir(kind, options) {
177
- const override = envValue(options, kind);
178
- if (override)
124
+ function pathsResolverBaseDir(kind, options) {
125
+ pathsResolverAssertKind(kind);
126
+ const env = options.env ?? process.env;
127
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
128
+ if (typeof override === "string" && override.length > 0)
179
129
  return override;
180
- const home = options.home ?? homedir();
130
+ const home = options.home ?? pathsResolverHomedir();
181
131
  const platform = options.platform ?? process.platform;
182
- if (isMacOS(platform)) {
132
+ if (platform === "darwin") {
183
133
  switch (kind) {
184
- case "config":
185
134
  case "data":
186
- return join(home, "Library", "Application Support", "Hasna");
135
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
187
136
  case "cache":
188
- return join(home, "Library", "Caches", "Hasna");
137
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
189
138
  case "state":
190
- return join(home, "Library", "Logs", "Hasna");
139
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
191
140
  }
192
141
  }
193
142
  switch (kind) {
194
- case "config":
195
- return join(home, ".config", "hasna");
196
143
  case "data":
197
- return join(home, ".local", "share", "hasna");
144
+ return pathsResolverJoin(home, ".local", "share", "hasna");
198
145
  case "state":
199
- return join(home, ".local", "state", "hasna");
146
+ return pathsResolverJoin(home, ".local", "state", "hasna");
200
147
  case "cache":
201
- return join(home, ".cache", "hasna");
148
+ return pathsResolverJoin(home, ".cache", "hasna");
202
149
  }
203
150
  }
204
- function resolvePath(kind, options) {
205
- assertApp(options.app);
206
- const appSegment = options.internal === true ? join("internal", options.app) : options.app;
207
- return join(baseDir(kind, options), appSegment);
151
+ function pathsResolverResolve(kind, options) {
152
+ pathsResolverAssertApp(options.app);
153
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
154
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
208
155
  }
209
156
  function dataDir(options) {
210
- return resolvePath("data", options);
157
+ return pathsResolverResolve("data", options);
211
158
  }
212
- var KIND_ENV, APP_SLUG_RE;
213
- var init_dist = __esm(() => {
214
- KIND_ENV = {
215
- config: "HASNA_CONFIG_HOME",
216
- data: "HASNA_DATA_HOME",
217
- state: "HASNA_STATE_HOME",
218
- cache: "HASNA_CACHE_HOME"
219
- };
220
- APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
221
- });
222
-
223
- // src/lib/paths.ts
224
- import { existsSync } from "fs";
225
- import { homedir as homedir2 } from "os";
226
- import { join as join2, resolve } from "path";
227
159
  function effectiveHome() {
228
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir2() || "/tmp";
160
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir() || "/tmp";
229
161
  }
230
162
  function legacyDataRoot() {
231
- return join2(effectiveHome(), ".hasna", "mementos");
163
+ return join(effectiveHome(), ".hasna", "mementos");
232
164
  }
233
165
  function resolverDataRoot() {
234
166
  return dataDir({
@@ -240,7 +172,7 @@ function adoptResolverDataRoot(resolved, env = process.env) {
240
172
  const dataOverride = env.HASNA_DATA_HOME;
241
173
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
242
174
  return true;
243
- return existsSync(join2(resolved, "mementos.db"));
175
+ return existsSync(join(resolved, "mementos.db"));
244
176
  }
245
177
  function exactDataRoot() {
246
178
  for (const key of ["HASNA_MEMENTOS_HOME", "MEMENTOS_HOME"]) {
@@ -257,14 +189,34 @@ function getDataRoot() {
257
189
  const resolved = resolverDataRoot();
258
190
  return adoptResolverDataRoot(resolved) ? resolve(resolved) : resolve(legacyDataRoot());
259
191
  }
192
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
260
193
  var init_paths = __esm(() => {
261
- init_dist();
194
+ PATHS_RESOLVER_KIND_ENV = {
195
+ data: "HASNA_DATA_HOME",
196
+ state: "HASNA_STATE_HOME",
197
+ cache: "HASNA_CACHE_HOME"
198
+ };
199
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
200
+ });
201
+
202
+ // src/lib/env.ts
203
+ var alias = (canonical, legacy) => process.env[canonical] ?? process.env[legacy], env;
204
+ var init_env = __esm(() => {
205
+ env = {
206
+ apiKey: () => alias("HASNA_MEMENTOS_API_KEY", "MEMENTOS_API_KEY"),
207
+ defaultScope: () => alias("HASNA_MEMENTOS_DEFAULT_SCOPE", "MEMENTOS_DEFAULT_SCOPE"),
208
+ defaultCategory: () => alias("HASNA_MEMENTOS_DEFAULT_CATEGORY", "MEMENTOS_DEFAULT_CATEGORY"),
209
+ defaultImportance: () => alias("HASNA_MEMENTOS_DEFAULT_IMPORTANCE", "MEMENTOS_DEFAULT_IMPORTANCE"),
210
+ pgsyncQueryTimeoutMs: () => alias("HASNA_MEMENTOS_PGSYNC_QUERY_TIMEOUT_MS", "MEMENTOS_PGSYNC_QUERY_TIMEOUT_MS"),
211
+ reflectProvider: () => alias("HASNA_MEMENTOS_REFLECT_PROVIDER", "MEMENTOS_REFLECT_PROVIDER"),
212
+ reflectModel: () => alias("HASNA_MEMENTOS_REFLECT_MODEL", "MEMENTOS_REFLECT_MODEL")
213
+ };
262
214
  });
263
215
 
264
216
  // src/storage.ts
265
217
  import { Database } from "bun:sqlite";
266
218
  import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
267
- import { join as join3 } from "path";
219
+ import { join as join2 } from "path";
268
220
  import { fileURLToPath } from "url";
269
221
  import { Worker } from "worker_threads";
270
222
  import pg from "pg";
@@ -289,6 +241,7 @@ class SqliteAdapter {
289
241
  db;
290
242
  constructor(path) {
291
243
  this.db = new Database(path, { create: true });
244
+ this.db.exec("PRAGMA busy_timeout = 5000");
292
245
  this.db.exec("PRAGMA journal_mode = WAL");
293
246
  this.db.exec("PRAGMA foreign_keys = ON");
294
247
  }
@@ -539,9 +492,9 @@ function getConfigPath() {
539
492
  return STORAGE_CONFIG_PATH;
540
493
  }
541
494
  function getStorageDatabaseEnv() {
542
- for (const env of DATABASE_ENV_NAMES) {
543
- if (readEnv(env.name))
544
- return env;
495
+ for (const env2 of DATABASE_ENV_NAMES) {
496
+ if (readEnv(env2.name))
497
+ return env2;
545
498
  }
546
499
  return null;
547
500
  }
@@ -551,20 +504,19 @@ function getStorageEnvName(key) {
551
504
  return readEnv(canonical) || !readEnv(fallback) ? canonical : fallback;
552
505
  }
553
506
  function getStorageDatabaseUrl() {
554
- const env = getStorageDatabaseEnv();
555
- return env ? readEnv(env.name) : null;
507
+ const env2 = getStorageDatabaseEnv();
508
+ return env2 ? readEnv(env2.name) : null;
556
509
  }
557
510
  function getStorageDatabaseEnvName() {
558
511
  return getStorageEnvName("databaseUrl");
559
512
  }
560
- function getStorageBackend(env = process.env) {
561
- return resolveServerDataBackend("mementos", env).backend;
513
+ function getStorageBackend(env2 = process.env) {
514
+ return resolveServerDataBackend("mementos", env2).backend;
562
515
  }
563
- function getStorageBackendDatabaseUrl(env = process.env) {
564
- return resolveDatabaseUrl("mementos", env);
516
+ function getStorageBackendDatabaseUrl(env2 = process.env) {
517
+ return resolveDatabaseUrl("mementos", env2);
565
518
  }
566
519
  function getStorageConfig() {
567
- assertNoLegacyStorageMode2();
568
520
  const fileConfig = readConfigFile();
569
521
  const merged = {
570
522
  ...DEFAULT_STORAGE_CONFIG,
@@ -651,14 +603,14 @@ function storageEnvStatus(key) {
651
603
  };
652
604
  }
653
605
  function postgresBackendStatus(config) {
654
- const env = getStorageDatabaseEnv();
655
- const envUrl = env ? readEnv(env.name) : null;
656
- if (env && envUrl) {
606
+ const env2 = getStorageDatabaseEnv();
607
+ const envUrl = env2 ? readEnv(env2.name) : null;
608
+ if (env2 && envUrl) {
657
609
  const validation = validatePostgresConnectionString(envUrl);
658
610
  return {
659
611
  configured: validation.ok,
660
612
  source: "env",
661
- env_name: env.name,
613
+ env_name: env2.name,
662
614
  redacted_url: validation.redacted_url,
663
615
  missing: [],
664
616
  issues: validation.issues,
@@ -704,7 +656,6 @@ function getSafeStorageConfigSummary(config = getStorageConfig()) {
704
656
  };
705
657
  }
706
658
  function getStorageStatus() {
707
- assertNoLegacyStorageMode2();
708
659
  const config = getStorageConfig();
709
660
  const backend = getStorageBackend();
710
661
  const postgresRequested = backend === "postgresql";
@@ -814,14 +765,12 @@ function resolveConfiguredConnectionString(dbName) {
814
765
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
815
766
  }
816
767
  function getStorageConnectionString(dbName = "mementos") {
817
- assertNoLegacyStorageMode2();
818
768
  if (!isServerContext()) {
819
769
  throw new Error("Refusing to construct an RDS Postgres DSN outside the mementos-serve server. " + "The raw database DSN is NEVER distributed to client machines. " + "Clients must use the HTTP API: set HASNA_MEMENTOS_API_URL and " + "HASNA_MEMENTOS_API_KEY (and unset HASNA_MEMENTOS_DATABASE_URL).");
820
770
  }
821
771
  return resolveConfiguredConnectionString(dbName);
822
772
  }
823
773
  function getStorageConnectionStringForOperator(dbName = "mementos") {
824
- assertNoLegacyStorageMode2();
825
774
  return resolveConfiguredConnectionString(dbName);
826
775
  }
827
776
  function isSyncExcludedTable(table) {
@@ -992,8 +941,8 @@ CREATE TABLE IF NOT EXISTS _sync_meta (
992
941
  )`;
993
942
  var init_storage = __esm(() => {
994
943
  init_backend();
995
- init_retired_storage_mode();
996
944
  init_paths();
945
+ init_env();
997
946
  PgSyncPool = class PgSyncPool {
998
947
  worker;
999
948
  status;
@@ -1003,7 +952,7 @@ var init_storage = __esm(() => {
1003
952
  generation = 0;
1004
953
  static DATA_BYTES = 128 * 1024 * 1024;
1005
954
  static queryTimeoutMs() {
1006
- const raw = process.env["MEMENTOS_PGSYNC_QUERY_TIMEOUT_MS"]?.trim();
955
+ const raw = env.pgsyncQueryTimeoutMs()?.trim();
1007
956
  const parsed = raw ? Number(raw) : Number.NaN;
1008
957
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 60000;
1009
958
  }
@@ -1011,9 +960,9 @@ var init_storage = __esm(() => {
1011
960
  const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
1012
961
  const here = fileURLToPath(new URL(".", import.meta.url));
1013
962
  const candidates = [
1014
- join3(here, `pg-sync-worker${ext}`),
1015
- join3(here, "..", `pg-sync-worker${ext}`),
1016
- join3(here, "..", "..", `pg-sync-worker${ext}`)
963
+ join2(here, `pg-sync-worker${ext}`),
964
+ join2(here, "..", `pg-sync-worker${ext}`),
965
+ join2(here, "..", "..", `pg-sync-worker${ext}`)
1017
966
  ];
1018
967
  for (const candidate of candidates) {
1019
968
  if (existsSync2(candidate))
@@ -1119,8 +1068,8 @@ var init_storage = __esm(() => {
1119
1068
  schedule_minutes: 0
1120
1069
  }
1121
1070
  };
1122
- STORAGE_CONFIG_DIR = join3(LOCAL_DATA_DIR, "storage");
1123
- STORAGE_CONFIG_PATH = join3(STORAGE_CONFIG_DIR, "config.json");
1071
+ STORAGE_CONFIG_DIR = join2(LOCAL_DATA_DIR, "storage");
1072
+ STORAGE_CONFIG_PATH = join2(STORAGE_CONFIG_DIR, "config.json");
1124
1073
  DATABASE_ENV_NAMES = [
1125
1074
  { name: MEMENTOS_STORAGE_ENV.databaseUrl, deprecated: false },
1126
1075
  { name: MEMENTOS_STORAGE_FALLBACK_ENV.databaseUrl, deprecated: false }
@@ -1144,28 +1093,96 @@ var init_storage = __esm(() => {
1144
1093
  ];
1145
1094
  });
1146
1095
 
1096
+ // src/lib/local-opt-in.ts
1097
+ import {
1098
+ clientTransportEnvKeys,
1099
+ credentialOverrideEnvKey,
1100
+ credentialPointerEnvKey,
1101
+ CREDENTIAL_PROFILE_ENV_KEY
1102
+ } from "@hasna/contracts/client";
1103
+ function isMementosLocalOptIn(env2 = process.env) {
1104
+ return MEMENTOS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env2[key] ?? "").trim() !== "");
1105
+ }
1106
+ function hasExplicitLocalDbPath(env2 = process.env) {
1107
+ return MEMENTOS_DB_PATH_ENV_KEYS.some((key) => (env2[key] ?? "").trim() !== "");
1108
+ }
1109
+ function mementosAuthorityEnvKeys() {
1110
+ const keys = clientTransportEnvKeys("mementos");
1111
+ return [
1112
+ ...keys.apiUrlKeys,
1113
+ ...keys.apiKeyKeys,
1114
+ credentialOverrideEnvKey("mementos"),
1115
+ credentialPointerEnvKey("mementos"),
1116
+ CREDENTIAL_PROFILE_ENV_KEY
1117
+ ];
1118
+ }
1119
+ function hasMementosEnvAuthorityIntent(env2 = process.env) {
1120
+ return mementosAuthorityEnvKeys().some((key) => (env2[key] ?? "").trim() !== "");
1121
+ }
1122
+ function selectsMementosLocalStore(env2 = process.env) {
1123
+ if (hasExplicitLocalDbPath(env2))
1124
+ return true;
1125
+ return !hasMementosEnvAuthorityIntent(env2) && isMementosLocalOptIn(env2);
1126
+ }
1127
+ function mementosResolverEnv(env2) {
1128
+ const blanks = mementosAuthorityEnvKeys().filter((key) => (key in env2) && (env2[key] ?? "").trim() === "");
1129
+ if (blanks.length === 0)
1130
+ return env2;
1131
+ const next = { ...env2 };
1132
+ for (const key of blanks)
1133
+ delete next[key];
1134
+ return next;
1135
+ }
1136
+ function isAmbientMementosEnv(env2) {
1137
+ if (typeof process !== "undefined" && env2 === process.env)
1138
+ return true;
1139
+ return env2[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
1140
+ }
1141
+ function mementosResolverInputs(env2, credentials = {}) {
1142
+ const normalised = mementosResolverEnv(env2);
1143
+ if (normalised === env2)
1144
+ return { env: normalised, credentials };
1145
+ const keychain = { ...credentials.keychain };
1146
+ if (keychain.enabled === undefined && keychain.run === undefined) {
1147
+ keychain.enabled = isAmbientMementosEnv(env2);
1148
+ }
1149
+ return { env: normalised, credentials: { ...credentials, keychain } };
1150
+ }
1151
+ var MEMENTOS_LOCAL_OPT_IN_ENV_KEYS, MEMENTOS_DB_PATH_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
1152
+ var init_local_opt_in = __esm(() => {
1153
+ MEMENTOS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_MEMENTOS_LOCAL", "MEMENTOS_LOCAL"];
1154
+ MEMENTOS_DB_PATH_ENV_KEYS = ["HASNA_MEMENTOS_DB_PATH", "MEMENTOS_DB_PATH"];
1155
+ CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
1156
+ });
1157
+
1147
1158
  // src/db/api-mode.ts
1148
1159
  import { tmpdir } from "os";
1149
- import { join as join4 } from "path";
1160
+ import { join as join3 } from "path";
1150
1161
  import { writeFileSync as writeFileSync2, unlinkSync } from "fs";
1151
1162
  import { randomUUID } from "crypto";
1152
- function firstEnv2(keys) {
1163
+ import {
1164
+ clientTransportEnvKeys as clientTransportEnvKeys2,
1165
+ resolveClientTransport,
1166
+ resolveCredential,
1167
+ toV1BaseUrl,
1168
+ ClientTransportConfigurationError
1169
+ } from "@hasna/contracts/client";
1170
+ function firstEnv(keys, env2 = process.env) {
1153
1171
  for (const k of keys) {
1154
- const v = process.env[k]?.trim();
1172
+ const v = env2[k]?.trim();
1155
1173
  if (v)
1156
1174
  return v;
1157
1175
  }
1158
1176
  return;
1159
1177
  }
1160
- function firstEnvKey(keys) {
1161
- for (const k of keys) {
1162
- if (process.env[k]?.trim())
1163
- return k;
1164
- }
1165
- return null;
1178
+ function hasDatabaseUrl(env2 = process.env) {
1179
+ return Boolean(firstEnv(DATABASE_URL_ENV_KEYS, env2));
1166
1180
  }
1167
- function hasDatabaseUrl() {
1168
- return Boolean(firstEnv2(DATABASE_URL_ENV_KEYS));
1181
+ function assertCleanResolvedBase(baseUrl) {
1182
+ if (/[?#]/.test(baseUrl)) {
1183
+ throw new Error("mementos base URL must not contain userinfo, query, or fragment data");
1184
+ }
1185
+ return baseUrl;
1169
1186
  }
1170
1187
  function isLoopbackHost(rawHost) {
1171
1188
  const host = rawHost.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
@@ -1186,50 +1203,63 @@ function assertRequestAllowedUnderTest(baseUrl) {
1186
1203
  return;
1187
1204
  throw new Error("api-mode: REFUSING to make a cloud request from a test process \u2014 this would write to or read " + "from the SHARED PRODUCTION memory store, where test fixtures are indistinguishable from real " + `memories.
1188
1205
  ` + ` host : ${host || "(unparseable base URL)"}
1189
- ` + ` how this happens: a selector set at module scope (after the bun test preload ran), or \`bun test\` ` + `invoked from a directory with no bunfig.toml so the preload never loaded.
1206
+ ` + " how this happens: a selector set at module scope (after the bun test preload ran), or `bun test` " + `invoked from a directory with no bunfig.toml so the preload never loaded.
1190
1207
  ` + " fix : build the child/process env via src/test-support/store-isolation.ts, or point the " + `suite at a loopback stub.
1191
1208
  ` + ` override : set ${ALLOW_REMOTE_API_IN_TESTS_ENV}=1 only for a test that must reach a remote endpoint.`);
1192
1209
  }
1193
- function normalizeBase(raw) {
1194
- let base = raw.trim().replace(/\/+$/, "");
1195
- if (/\/(v1|api)$/.test(base))
1196
- return base;
1197
- return `${base}/v1`;
1210
+ function toStoreConfigError(error) {
1211
+ const message = error instanceof Error ? error.message : String(error);
1212
+ return new MementosStoreConfigError(`${message} Local SQLite is opt-in only (${DB_PATH_ENV_KEYS[0]}, or ${MEMENTOS_LOCAL_OPT_IN_ENV_KEYS[0]} = 1) and is disabled by default \u2014 failing closed`);
1198
1213
  }
1199
- function assertUnambiguousStoreEnv(env = process.env) {
1200
- assertNoLegacyStorageMode2(env);
1201
- if (firstEnvKey(DB_PATH_ENV_KEYS))
1202
- return;
1203
- if (hasDatabaseUrl())
1204
- return;
1205
- const urlKey = firstEnvKey(API_URL_ENV_KEYS);
1206
- const keyKey = firstEnvKey(API_KEY_ENV_KEYS);
1207
- if (urlKey && !keyKey) {
1208
- throw new MementosStoreConfigError(`${urlKey} points at the cloud memory store but ${API_KEY_ENV_KEYS[0]} is not set. ` + `Refusing to serve the on-box SQLite store in its place, because it holds a different ` + `dataset. Set ${API_KEY_ENV_KEYS[0]} to reach the cloud store. If you meant to use the ` + `on-box SQLite store, unset ${urlKey} or set ${DB_PATH_ENV_KEYS[0]} explicitly.`);
1209
- }
1210
- if (keyKey && !urlKey) {
1211
- throw new MementosStoreConfigError(`${keyKey} is set but ${API_URL_ENV_KEYS[0]} is not, so the cloud memory store cannot be ` + `reached. Refusing to serve the on-box SQLite store in its place, because it holds a ` + `different dataset. Set ${API_URL_ENV_KEYS[0]} to reach the cloud store. If you meant to ` + `use the on-box SQLite store, unset ${keyKey} or set ${DB_PATH_ENV_KEYS[0]} explicitly.`);
1212
- }
1213
- }
1214
- function getApiConfig() {
1215
- assertUnambiguousStoreEnv();
1216
- if (firstEnvKey(DB_PATH_ENV_KEYS))
1214
+ function getApiConfig(env2 = process.env, options = {}) {
1215
+ if (hasDatabaseUrl(env2))
1217
1216
  return null;
1218
- const rawBase = firstEnv2(API_URL_ENV_KEYS);
1219
- const apiKey = firstEnv2(API_KEY_ENV_KEYS);
1220
- if (!rawBase || !apiKey)
1217
+ if (selectsMementosLocalStore(env2))
1221
1218
  return null;
1222
- return { baseUrl: normalizeBase(rawBase), apiKey };
1219
+ const inputs = mementosResolverInputs(env2, options.credentials);
1220
+ let credential;
1221
+ try {
1222
+ credential = resolveCredential("mementos", inputs.env, inputs.credentials);
1223
+ } catch (error) {
1224
+ throw toStoreConfigError(error);
1225
+ }
1226
+ let resolution;
1227
+ try {
1228
+ resolution = resolveClientTransport("mementos", inputs.env, {
1229
+ credentials: credential ? { ...inputs.credentials, apiKey: credential.apiKey } : inputs.credentials
1230
+ });
1231
+ } catch (error) {
1232
+ if (error instanceof ClientTransportConfigurationError && credential === null && /is not set and no API key could be resolved/.test(error.message)) {
1233
+ return null;
1234
+ }
1235
+ throw toStoreConfigError(error);
1236
+ }
1237
+ return { baseUrl: assertCleanResolvedBase(resolution.baseUrl), apiKey: credential.apiKey };
1223
1238
  }
1224
1239
  function isApiMode() {
1225
1240
  if (hasDatabaseUrl())
1226
1241
  return false;
1242
+ if (isServerContext())
1243
+ return false;
1227
1244
  return getApiConfig() !== null;
1228
1245
  }
1246
+ function assertClientStoreConfigured(env2 = process.env, options = {}) {
1247
+ if (selectsMementosLocalStore(env2))
1248
+ return;
1249
+ if (hasDatabaseUrl(env2))
1250
+ return;
1251
+ const config = getApiConfig(env2, options);
1252
+ if (config !== null)
1253
+ return;
1254
+ throw new MementosStoreConfigError(unconfiguredStoreMessage());
1255
+ }
1256
+ function unconfiguredStoreMessage() {
1257
+ return "mementos is not configured to reach any memory store, and will NOT fall back to the " + "on-box SQLite store (~/.hasna/mementos/mementos.db). " + "No credential could be resolved from the Keychain item hasna.credentials.mementos.api-key " + "(macOS only), ~/.hasna/mementos/config/credentials, or " + `${API_KEY_ENV_KEYS[0]}; the authority would be the fleet gateway ${"https://api.hasna.com/mementos"} ` + `(or ${API_URL_ENV_KEYS[0]} if set). ` + `Set ${API_URL_ENV_KEYS[0]} and ${API_KEY_ENV_KEYS[0]} (the resolver also accepts the legacy ` + `${API_URL_ENV_KEYS[1]} / ${API_KEY_ENV_KEYS[1]} aliases for one release) to use the fleet memory ` + `API, or opt into an explicit local SQLite file with ${DB_PATH_ENV_KEYS[0]} / ` + `${DB_PATH_ENV_KEYS[1]} (or ${MEMENTOS_LOCAL_OPT_IN_ENV_KEYS[0]}=1).`;
1258
+ }
1229
1259
  function apiRequestRaw(method, path, body) {
1230
1260
  const cfg = getApiConfig();
1231
1261
  if (!cfg)
1232
- throw new Error("api-mode: not configured (HASNA_MEMENTOS_API_URL / HASNA_MEMENTOS_API_KEY)");
1262
+ throw new Error("api-mode: not configured (no Hasna mementos credential resolved)");
1233
1263
  assertRequestAllowedUnderTest(cfg.baseUrl);
1234
1264
  const url = `${cfg.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
1235
1265
  const hasBody = body !== undefined && body !== null;
@@ -1256,8 +1286,15 @@ x-api-key: ${cfg.apiKey}
1256
1286
  ];
1257
1287
  let bodyFile;
1258
1288
  if (hasBody) {
1259
- bodyFile = join4(tmpdir(), `mem-req-${process.pid}-${randomUUID()}.json`);
1260
- writeFileSync2(bodyFile, JSON.stringify(body), { mode: 384 });
1289
+ bodyFile = join3(tmpdir(), `mem-req-${process.pid}-${randomUUID()}.json`);
1290
+ try {
1291
+ writeFileSync2(bodyFile, JSON.stringify(body), { mode: 384 });
1292
+ } catch (error) {
1293
+ try {
1294
+ unlinkSync(bodyFile);
1295
+ } catch {}
1296
+ throw error;
1297
+ }
1261
1298
  args.push("--data-binary", `@${bodyFile}`);
1262
1299
  }
1263
1300
  args.push(url);
@@ -1305,7 +1342,7 @@ function apiJson(method, path, body, options) {
1305
1342
  try {
1306
1343
  data = JSON.parse(raw.body);
1307
1344
  } catch (e) {
1308
- 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, "");
1309
1346
  }
1310
1347
  } else {
1311
1348
  data = undefined;
@@ -1344,13 +1381,14 @@ function toQuery(params) {
1344
1381
  const s = sp.toString();
1345
1382
  return s ? `?${s}` : "";
1346
1383
  }
1347
- var API_URL_ENV_KEYS, API_KEY_ENV_KEYS, DATABASE_URL_ENV_KEYS, ALLOW_REMOTE_API_IN_TESTS_ENV = "MEMENTOS_ALLOW_REMOTE_API_IN_TESTS", DB_PATH_ENV_KEYS, MementosStoreConfigError, ApiRequestError, DEFAULT_TIMEOUT_S = "45";
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";
1348
1385
  var init_api_mode = __esm(() => {
1349
- init_retired_storage_mode();
1350
- API_URL_ENV_KEYS = ["HASNA_MEMENTOS_API_URL", "MEMENTOS_API_URL"];
1351
- API_KEY_ENV_KEYS = ["HASNA_MEMENTOS_API_KEY", "MEMENTOS_API_KEY"];
1386
+ init_local_opt_in();
1387
+ init_storage();
1388
+ API_URL_ENV_KEYS = clientTransportEnvKeys2("mementos").apiUrlKeys;
1389
+ API_KEY_ENV_KEYS = clientTransportEnvKeys2("mementos").apiKeyKeys;
1352
1390
  DATABASE_URL_ENV_KEYS = ["HASNA_MEMENTOS_DATABASE_URL", "MEMENTOS_DATABASE_URL"];
1353
- DB_PATH_ENV_KEYS = ["HASNA_MEMENTOS_DB_PATH", "MEMENTOS_DB_PATH"];
1391
+ DB_PATH_ENV_KEYS = MEMENTOS_DB_PATH_ENV_KEYS;
1354
1392
  MementosStoreConfigError = class MementosStoreConfigError extends Error {
1355
1393
  code = "MEMENTOS_STORE_CONFIG";
1356
1394
  constructor(message) {
@@ -2759,6 +2797,220 @@ INSERT OR IGNORE INTO _migrations (id) VALUES (38);
2759
2797
  `
2760
2798
  ${sqliteMementosMemoryProjectLinkSchemaSql()}
2761
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);
2762
3014
  `
2763
3015
  ];
2764
3016
  });
@@ -2774,10 +3026,11 @@ __export(exports_database, {
2774
3026
  getDbPath: () => getDbPath,
2775
3027
  getDatabase: () => getDatabase,
2776
3028
  escapeLikePrefix: () => escapeLikePrefix,
2777
- closeDatabase: () => closeDatabase
3029
+ closeDatabase: () => closeDatabase,
3030
+ applyMachineIdentityMigration: () => applyMachineIdentityMigration
2778
3031
  });
2779
3032
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, cpSync } from "fs";
2780
- import { dirname, join as join5, resolve as resolve2 } from "path";
3033
+ import { dirname, join as join4, resolve as resolve2 } from "path";
2781
3034
  function isInMemoryDb(path) {
2782
3035
  return path === ":memory:" || path.startsWith("file::memory:");
2783
3036
  }
@@ -2786,7 +3039,7 @@ function findNearestMementosDb(startDir) {
2786
3039
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2787
3040
  const legacyHomeDb = resolve2(home, ".mementos", "mementos.db");
2788
3041
  while (true) {
2789
- const candidate = join5(dir, ".mementos", "mementos.db");
3042
+ const candidate = join4(dir, ".mementos", "mementos.db");
2790
3043
  if (existsSync3(candidate) && resolve2(candidate) !== legacyHomeDb)
2791
3044
  return candidate;
2792
3045
  const parent = dirname(dir);
@@ -2799,7 +3052,7 @@ function findNearestMementosDb(startDir) {
2799
3052
  function findGitRoot(startDir) {
2800
3053
  let dir = resolve2(startDir);
2801
3054
  while (true) {
2802
- if (existsSync3(join5(dir, ".git")))
3055
+ if (existsSync3(join4(dir, ".git")))
2803
3056
  return dir;
2804
3057
  const parent = dirname(dir);
2805
3058
  if (parent === dir)
@@ -2809,9 +3062,11 @@ function findGitRoot(startDir) {
2809
3062
  return null;
2810
3063
  }
2811
3064
  function migrateGlobalDir() {
3065
+ if (!selectsMementosLocalStore())
3066
+ return;
2812
3067
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2813
3068
  const newDir = getDataRoot();
2814
- const oldDir = join5(home, ".mementos");
3069
+ const oldDir = join4(home, ".mementos");
2815
3070
  if (!existsSync3(newDir) && existsSync3(oldDir)) {
2816
3071
  mkdirSync2(dirname(newDir), { recursive: true });
2817
3072
  cpSync(oldDir, newDir, { recursive: true });
@@ -2830,11 +3085,11 @@ function getDbPath() {
2830
3085
  if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
2831
3086
  const gitRoot = findGitRoot(cwd);
2832
3087
  if (gitRoot) {
2833
- return join5(gitRoot, ".mementos", "mementos.db");
3088
+ return join4(gitRoot, ".mementos", "mementos.db");
2834
3089
  }
2835
3090
  }
2836
3091
  migrateGlobalDir();
2837
- return join5(getDataRoot(), "mementos.db");
3092
+ return join4(getDataRoot(), "mementos.db");
2838
3093
  }
2839
3094
  function ensureDir(filePath) {
2840
3095
  if (isInMemoryDb(filePath))
@@ -2870,6 +3125,9 @@ function getDatabase(dbPath) {
2870
3125
  throw new Error("REFUSING-UNPINNED-TEST-OPEN: a test process tried to open the default local " + `SQLite store (${getDbPath()}) with no explicit dbPath argument and no ` + `${DB_PATH_ENV_KEYS.join("/")} set. Pass an explicit dbPath (e.g. ":memory:" ` + "or a scratch file), or set one of those env keys to a scratch path, before " + "calling getDatabase(). This guard exists because an unpinned test process " + "would otherwise silently read and write the real, shared, on-disk memory " + "store (todos 57b8b8c5).");
2871
3126
  }
2872
3127
  }
3128
+ if (process.env["NODE_ENV"] !== "test" && !isServerContext()) {
3129
+ assertClientStoreConfigured();
3130
+ }
2873
3131
  }
2874
3132
  const path = dbPath || getDbPath();
2875
3133
  if (_db) {
@@ -2882,7 +3140,6 @@ function getDatabase(dbPath) {
2882
3140
  ensureDir(path);
2883
3141
  _db = new SqliteAdapter(path);
2884
3142
  _db.run("PRAGMA journal_mode = WAL");
2885
- _db.run("PRAGMA busy_timeout = 5000");
2886
3143
  _db.run("PRAGMA foreign_keys = ON");
2887
3144
  _db.run("PRAGMA wal_autocheckpoint = 100");
2888
3145
  runMigrations(_db);
@@ -2898,32 +3155,62 @@ function getDatabase(dbPath) {
2898
3155
  return _db;
2899
3156
  }
2900
3157
  function runMigrations(db) {
3158
+ let currentLevel = 0;
2901
3159
  try {
2902
3160
  const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
2903
- const currentLevel = result?.max_id ?? 0;
2904
- for (let i = currentLevel;i < MIGRATIONS.length; i++) {
2905
- try {
2906
- applyMigration(db, i);
2907
- } catch (e) {
2908
- console.warn(`[mementos] Migration ${i + 1} failed: ${e instanceof Error ? e.message : String(e)}`);
2909
- }
2910
- }
2911
- } catch {
2912
- for (let i = 0;i < MIGRATIONS.length; i++) {
2913
- try {
2914
- applyMigration(db, i);
2915
- } catch (e) {
2916
- 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}`);
2917
3170
  }
3171
+ console.warn(`[mementos] Migration ${i + 1} failed: ${message}`);
2918
3172
  }
2919
3173
  }
2920
3174
  }
2921
3175
  function applyMigration(db, index) {
2922
- if (index === 32) {
3176
+ if (index === 32)
2923
3177
  ensureMachinePrimaryColumn(db);
3178
+ if (index === 40) {
3179
+ applyMachineIdentityMigration(db);
3180
+ return;
2924
3181
  }
2925
3182
  db.exec(MIGRATIONS[index]);
2926
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
+ }
2927
3214
  function ensureMachinePrimaryColumn(db) {
2928
3215
  const columns = db.query("PRAGMA table_info(machines)").all();
2929
3216
  if (columns.length === 0 || columns.some((column) => column.name === "is_primary")) {
@@ -2979,6 +3266,7 @@ var init_database = __esm(() => {
2979
3266
  init_api_mode();
2980
3267
  init_migrations();
2981
3268
  init_paths();
3269
+ init_local_opt_in();
2982
3270
  ALLOWED_TABLES = new Set([
2983
3271
  "memories",
2984
3272
  "agents",
@@ -3007,13 +3295,13 @@ import { createHash } from "crypto";
3007
3295
 
3008
3296
  // src/lib/package-version.ts
3009
3297
  import { readFileSync as readFileSync2 } from "fs";
3010
- import { dirname as dirname2, join as join6 } from "path";
3298
+ import { dirname as dirname2, join as join5 } from "path";
3011
3299
  import { fileURLToPath as fileURLToPath2 } from "url";
3012
3300
  function getMementosPackageVersion() {
3013
3301
  const here = dirname2(fileURLToPath2(import.meta.url));
3014
3302
  for (const candidate of [
3015
- join6(here, "..", "..", "package.json"),
3016
- join6(here, "..", "package.json")
3303
+ join5(here, "..", "..", "package.json"),
3304
+ join5(here, "..", "package.json")
3017
3305
  ]) {
3018
3306
  try {
3019
3307
  const parsed = JSON.parse(readFileSync2(candidate, "utf8"));
@@ -5200,6 +5488,17 @@ var SECRET_PATTERNS = [
5200
5488
  { name: "jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
5201
5489
  { name: "hex_secret", pattern: /(?<=(?:key|token|secret|password|hash)\s*[:=]\s*["']?)[0-9a-f]{32,}(?=["']?)/gi }
5202
5490
  ];
5491
+ var KEY_TAG_SECRET_PATTERNS = SECRET_PATTERNS.filter((p) => new Set([
5492
+ "openai_key",
5493
+ "anthropic_key",
5494
+ "aws_key",
5495
+ "github_token",
5496
+ "github_oauth",
5497
+ "npm_token",
5498
+ "stripe_key",
5499
+ "slack_token",
5500
+ "jwt"
5501
+ ]).has(p.name));
5203
5502
  function redactSecrets(text) {
5204
5503
  let result = text;
5205
5504
  for (const { pattern } of SECRET_PATTERNS) {
@@ -5208,6 +5507,14 @@ function redactSecrets(text) {
5208
5507
  }
5209
5508
  return result;
5210
5509
  }
5510
+ function redactCredentialKey(text) {
5511
+ let result = text;
5512
+ for (const { pattern } of KEY_TAG_SECRET_PATTERNS) {
5513
+ pattern.lastIndex = 0;
5514
+ result = result.replace(pattern, REDACTED);
5515
+ }
5516
+ return result;
5517
+ }
5211
5518
  function containsSecrets(text) {
5212
5519
  for (const { pattern } of SECRET_PATTERNS) {
5213
5520
  pattern.lastIndex = 0;
@@ -5216,6 +5523,40 @@ function containsSecrets(text) {
5216
5523
  }
5217
5524
  return false;
5218
5525
  }
5526
+ function redactValueTree(value) {
5527
+ if (typeof value === "string")
5528
+ return redactSecrets(value);
5529
+ if (Array.isArray(value))
5530
+ return value.map(redactValueTree);
5531
+ if (value !== null && typeof value === "object") {
5532
+ const out = {};
5533
+ for (const [k, v] of Object.entries(value)) {
5534
+ out[k] = redactValueTree(v);
5535
+ }
5536
+ return out;
5537
+ }
5538
+ return value;
5539
+ }
5540
+ function redactMemoryWriteInput(input) {
5541
+ const out = { ...input };
5542
+ if (typeof out.key === "string")
5543
+ out.key = redactCredentialKey(out.key);
5544
+ if (typeof out.value === "string")
5545
+ out.value = redactSecrets(out.value);
5546
+ if (out.summary !== undefined && out.summary !== null) {
5547
+ out.summary = redactSecrets(String(out.summary));
5548
+ }
5549
+ if (Array.isArray(out.tags)) {
5550
+ out.tags = out.tags.map((t) => redactCredentialKey(String(t)));
5551
+ }
5552
+ if (typeof out.when_to_use === "string") {
5553
+ out.when_to_use = redactSecrets(out.when_to_use);
5554
+ }
5555
+ if (out.metadata !== undefined) {
5556
+ out.metadata = redactValueTree(out.metadata);
5557
+ }
5558
+ return out;
5559
+ }
5219
5560
 
5220
5561
  // src/lib/hooks.ts
5221
5562
  var _idCounter = 0;
@@ -5528,6 +5869,7 @@ function createMemory(input, dedupeMode = "merge", db) {
5528
5869
  if (reservedViolation) {
5529
5870
  throw new Error(reservedViolation);
5530
5871
  }
5872
+ input = redactMemoryWriteInput(input);
5531
5873
  if (!db && isApiMode()) {
5532
5874
  const { status, data } = apiJson("POST", "/memories", { ...input, dedupe: dedupeMode });
5533
5875
  if (!data || !data.id) {
@@ -5820,7 +6162,7 @@ function buildMemoryListConditions(filter) {
5820
6162
  conditions.push("status = 'active'");
5821
6163
  }
5822
6164
  if (filter.project_id) {
5823
- conditions.push("project_id = ?");
6165
+ conditions.push(filter.include_unassigned_project === true ? "(project_id = ? OR project_id IS NULL)" : "project_id = ?");
5824
6166
  params.push(filter.project_id);
5825
6167
  }
5826
6168
  if (filter.agent_id) {
@@ -5900,6 +6242,7 @@ function listMemoriesPage(filter, db) {
5900
6242
  pinned: f.pinned,
5901
6243
  agent_id: f.agent_id,
5902
6244
  project_id: f.project_id,
6245
+ include_unassigned_project: f.project_id && f.include_unassigned_project === true ? true : undefined,
5903
6246
  session_id: f.session_id,
5904
6247
  machine_id: f.machine_id,
5905
6248
  visible_to_machine_id: f.visible_to_machine_id,
@@ -5924,7 +6267,7 @@ function listMemoriesPage(filter, db) {
5924
6267
  if (conditions.length > 0) {
5925
6268
  sql += ` WHERE ${conditions.join(" AND ")}`;
5926
6269
  }
5927
- sql += " ORDER BY importance DESC, created_at DESC";
6270
+ sql += " ORDER BY importance DESC, created_at DESC, id DESC";
5928
6271
  if (f.limit) {
5929
6272
  sql += " LIMIT ?";
5930
6273
  params.push(f.limit);
@@ -5985,6 +6328,7 @@ function listMemoriesBounded(filter = {}, target, db) {
5985
6328
  };
5986
6329
  }
5987
6330
  function updateMemory(id, input, db) {
6331
+ input = redactMemoryWriteInput(input);
5988
6332
  if (!db && isApiMode()) {
5989
6333
  const { status, data } = apiJson("PATCH", `/memories/${encodeURIComponent(id)}`, input, { allow404: true });
5990
6334
  if (status === 404)