om-memory-system 4.25.0 → 4.26.1

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 (84) hide show
  1. package/README.md +17 -15
  2. package/dist/adapters/opencode/auto-capture-summary.js +1 -1
  3. package/dist/adapters/opencode/import-command.js +2 -2
  4. package/dist/adapters/opencode/profile-learning.js +9 -9
  5. package/dist/adapters/opencode/profile-model.js +1 -1
  6. package/dist/adapters/pi/capture.js +2 -2
  7. package/dist/adapters/pi/extension.js +8 -8
  8. package/dist/adapters/pi/import-command.js +2 -2
  9. package/dist/adapters/pi/live-model.js +1 -1
  10. package/dist/adapters/pi/profile.js +2 -2
  11. package/dist/cli/web-power.d.ts +2 -1
  12. package/dist/cli/web-power.js +2 -2
  13. package/dist/config.js +1 -1
  14. package/dist/core/profile-analysis.js +1 -1
  15. package/dist/core/profile-backoff.js +1 -1
  16. package/dist/importer/auto-backfill.js +14 -4
  17. package/dist/importer/backfill-controls.js +1 -1
  18. package/dist/importer/claude-hook-api.js +5 -5
  19. package/dist/importer/claude-profile-learning.js +1 -1
  20. package/dist/importer/import-readiness.d.ts +3 -0
  21. package/dist/importer/import-readiness.js +11 -4
  22. package/dist/importer/run-import.js +4 -3
  23. package/dist/index.js +19 -19
  24. package/dist/services/ai/providers/anthropic-messages.js +2 -2
  25. package/dist/services/ai/providers/google-gemini.js +1 -1
  26. package/dist/services/ai/providers/openai-chat-completion.js +3 -3
  27. package/dist/services/ai/providers/openai-responses.js +2 -2
  28. package/dist/services/ai-cleanup-handlers.js +1 -1
  29. package/dist/services/api-handlers.js +27 -27
  30. package/dist/services/auto-capture.js +3 -3
  31. package/dist/services/auto-cleanup-handlers.js +1 -1
  32. package/dist/services/backfill-state.d.ts +8 -2
  33. package/dist/services/backfill-state.js +14 -4
  34. package/dist/services/capture-diagnostics.js +3 -3
  35. package/dist/services/capture-retry-drain.js +5 -5
  36. package/dist/services/capture-retry-queue.js +1 -1
  37. package/dist/services/chat/chat-routes.js +1 -1
  38. package/dist/services/cleanup-service.js +5 -5
  39. package/dist/services/client.js +10 -10
  40. package/dist/services/deduplication-service.js +1 -1
  41. package/dist/services/embedding.js +2 -2
  42. package/dist/services/host-appearance.d.ts +28 -0
  43. package/dist/services/host-appearance.js +117 -0
  44. package/dist/services/logger.d.ts +7 -1
  45. package/dist/services/logger.js +9 -3
  46. package/dist/services/memory-host-routes.d.ts +11 -0
  47. package/dist/services/memory-host-routes.js +56 -0
  48. package/dist/services/memory-portability-service.js +1 -1
  49. package/dist/services/migration-service.js +2 -2
  50. package/dist/services/settings-log.d.ts +23 -2
  51. package/dist/services/settings-log.js +65 -7
  52. package/dist/services/shard-path-migration-service.js +4 -4
  53. package/dist/services/tag-prefix-migration.js +1 -1
  54. package/dist/services/turso/connection-manager.js +3 -3
  55. package/dist/services/turso/keyword-search.js +1 -1
  56. package/dist/services/turso/legacy-migrator.js +1 -1
  57. package/dist/services/turso/ready.js +1 -1
  58. package/dist/services/turso/shard-manager.js +2 -2
  59. package/dist/services/turso/vector-search.js +3 -3
  60. package/dist/services/user-profile/ai-cleanup-batches.js +1 -1
  61. package/dist/services/user-profile/ai-cleanup-job.js +1 -1
  62. package/dist/services/user-profile/auto-cleanup.js +2 -2
  63. package/dist/services/user-profile/category-cleanup-job.js +1 -1
  64. package/dist/services/user-profile/category-cleanup.js +3 -3
  65. package/dist/services/user-profile/merge-and-save.js +1 -1
  66. package/dist/services/user-profile/profile-context.js +1 -1
  67. package/dist/services/user-profile/profile-model.js +1 -1
  68. package/dist/services/user-profile/user-profile-manager.js +12 -12
  69. package/dist/services/web-autostart.js +2 -2
  70. package/dist/services/web-route-modules.js +4 -0
  71. package/dist/services/web-server.js +13 -13
  72. package/dist/services/web-update.d.ts +2 -1
  73. package/dist/services/web-update.js +1 -1
  74. package/dist/services/write-health-checks.d.ts +3 -0
  75. package/dist/services/write-health-checks.js +20 -0
  76. package/dist/services/write-health.d.ts +26 -0
  77. package/dist/services/write-health.js +58 -0
  78. package/dist/v2/adapter.js +4 -4
  79. package/dist/web/assets/index-CIlyLof-.js +126 -0
  80. package/dist/web/assets/index-DH6qnFfk.css +2 -0
  81. package/dist/web/index.html +2 -2
  82. package/package.json +1 -1
  83. package/dist/web/assets/index-DWk9C9Sm.css +0 -2
  84. package/dist/web/assets/index-rHcTKCkC.js +0 -126
@@ -24,7 +24,7 @@ export async function ensureTursoReady() {
24
24
  }
25
25
  catch (error) {
26
26
  initPromise = null;
27
- log("Turso ready gate failed", { error: String(error) });
27
+ log("Turso ready gate failed", { error: String(error) }, "error");
28
28
  throw error;
29
29
  }
30
30
  })();
@@ -359,7 +359,7 @@ export class TursoShardManager {
359
359
  log("Error validating shard DB", {
360
360
  dbPath: shard.dbPath,
361
361
  error: String(error),
362
- });
362
+ }, "error");
363
363
  return false;
364
364
  }
365
365
  }
@@ -437,7 +437,7 @@ export class TursoShardManager {
437
437
  log("Error deleting shard file", {
438
438
  dbPath: fullPath,
439
439
  error: String(error),
440
- });
440
+ }, "error");
441
441
  }
442
442
  await metadataDb.run(`DELETE FROM shards WHERE id = ?`, [shardId]);
443
443
  }
@@ -199,7 +199,7 @@ export class TursoVectorSearch {
199
199
  log("Turso vector_top_k failed; falling back to exact scan", {
200
200
  indexName,
201
201
  error: String(error),
202
- });
202
+ }, "warn");
203
203
  return exactScanKind(db, queryJson, k, containerTag, columnName);
204
204
  }
205
205
  }
@@ -211,7 +211,7 @@ export class TursoVectorSearch {
211
211
  }
212
212
  catch (error) {
213
213
  const message = error instanceof Error ? error.message : String(error);
214
- log("Shard search error", { shardId: shard.id, error: message });
214
+ log("Shard search error", { shardId: shard.id, error: message }, "error");
215
215
  shardErrors.push({ shardId: shard.id, error: message });
216
216
  return [];
217
217
  }
@@ -231,7 +231,7 @@ export class TursoVectorSearch {
231
231
  log("Vector search completed with partial shard failures", {
232
232
  failedShards: shardErrors,
233
233
  totalShards: shards.length,
234
- });
234
+ }, "warn");
235
235
  }
236
236
  const allResults = resultsArray.flat();
237
237
  if (keywordMode === "rrf" && keywordBoost > 0) {
@@ -145,7 +145,7 @@ export async function runInBatches(batches, { callBatch = cleanupBatch, onProgre
145
145
  of: batches.length,
146
146
  items: countItems(batch),
147
147
  reason: failureReason(error),
148
- });
148
+ }, "error");
149
149
  }
150
150
  onProgress?.(index + 1, batches.length, joined.failedBatches);
151
151
  }
@@ -48,7 +48,7 @@ export function startAiCleanupJob(userId, input, deps = {}) {
48
48
  if (job.stopped)
49
49
  return;
50
50
  const reason = failureReason(error);
51
- log("AI cleanup job: failed", { reason });
51
+ log("AI cleanup job: failed", { reason }, "error");
52
52
  job.status = {
53
53
  state: "failed",
54
54
  error: reason === "timeout"
@@ -24,7 +24,7 @@ export async function startAutoProfileCleanup(userId, options = {}) {
24
24
  return await run(userId, options);
25
25
  }
26
26
  catch (error) {
27
- log("auto cleanup: failed", { reason: failureReason(error) });
27
+ log("auto cleanup: failed", { reason: failureReason(error) }, "error");
28
28
  return { status: "failed" };
29
29
  }
30
30
  finally {
@@ -58,7 +58,7 @@ async function run(userId, options) {
58
58
  return { status: "nothing-to-change" };
59
59
  }
60
60
  if (proposed > AUTO_CLEANUP_MAX_CHANGE_SHARE * sent) {
61
- log("auto cleanup: skipped", { reason: "too-many-changes", proposed, sent });
61
+ log("auto cleanup: skipped", { reason: "too-many-changes", proposed, sent }, "warn");
62
62
  return { status: "too-many-changes" };
63
63
  }
64
64
  const job = { old: data, changes: result.diff, mergedItems: result.mergedItems };
@@ -31,7 +31,7 @@ export function startCategoryCleanupJob(userId, deps = {}) {
31
31
  catch (error) {
32
32
  log("category cleanup job: failed", {
33
33
  code: error instanceof Error ? error.name : "unknown",
34
- });
34
+ }, "error");
35
35
  job.status = {
36
36
  state: "failed",
37
37
  error: error instanceof Error ? error.message : String(error),
@@ -129,7 +129,7 @@ onProgress, cache) {
129
129
  log("category cleanup: model batch failed", {
130
130
  groups: batch.length,
131
131
  code: error instanceof Error ? error.name : "unknown",
132
- });
132
+ }, "error");
133
133
  for (const group of batch)
134
134
  named.set(group.id, fallback(group));
135
135
  return;
@@ -150,7 +150,7 @@ onProgress, cache) {
150
150
  log("category cleanup: verdict save failed", {
151
151
  groups: verdicts.size,
152
152
  code: error instanceof Error ? error.name : "unknown",
153
- });
153
+ }, "error");
154
154
  }
155
155
  }
156
156
  };
@@ -267,7 +267,7 @@ async function openVerdictCache(profileId, groupIds) {
267
267
  catch (error) {
268
268
  log("category cleanup: verdict store unavailable", {
269
269
  code: error instanceof Error ? error.name : "unknown",
270
- });
270
+ }, "warn");
271
271
  return undefined;
272
272
  }
273
273
  }
@@ -9,7 +9,7 @@ export async function saveProfile(profile, data, summary, promptsAnalyzed = 0, s
9
9
  const target = store ?? (await import("./user-profile-manager.js")).userProfileManager;
10
10
  const saved = await target.updateProfile(profile.id, data, promptsAnalyzed, summary, profile.version);
11
11
  if (!saved)
12
- log("profile save skipped", { code: "version-changed" });
12
+ log("profile save skipped", { code: "version-changed" }, "warn");
13
13
  return saved;
14
14
  }
15
15
  /**
@@ -50,7 +50,7 @@ export async function getUserProfileContext(userId, queryVector) {
50
50
  }
51
51
  catch (e) {
52
52
  // A single corrupt row must not break context injection for everyone.
53
- log("profile context: failed to parse stored profileData", { error: String(e) });
53
+ log("profile context: failed to parse stored profileData", { error: String(e) }, "error");
54
54
  return null;
55
55
  }
56
56
  const parts = [];
@@ -19,7 +19,7 @@ export async function resolveHostProfileModel() {
19
19
  catch (error) {
20
20
  log("profile model: host model unavailable, falling back to external API", {
21
21
  error: String(error),
22
- });
22
+ }, "warn");
23
23
  return null;
24
24
  }
25
25
  }
@@ -103,7 +103,7 @@ export class UserProfileManager {
103
103
  catch (error) {
104
104
  this.initPromise = null;
105
105
  this.db = null;
106
- log("user-profile-manager: db init failed", { error: String(error) });
106
+ log("user-profile-manager: db init failed", { error: String(error) }, "error");
107
107
  throw error;
108
108
  }
109
109
  })();
@@ -508,7 +508,7 @@ export class UserProfileManager {
508
508
  // pair. They match only in the strong band and never absorb other workflows.
509
509
  const isWorkflow = itemType === "workflow";
510
510
  if (!useEmbedding) {
511
- log("profile embedding skipped: model not warmed up");
511
+ log("profile embedding skipped: model not warmed up", undefined, "warn");
512
512
  }
513
513
  for (const item of existing) {
514
514
  this.lazyMigrateAlpha(item);
@@ -530,7 +530,7 @@ export class UserProfileManager {
530
530
  log("profile centroid migration failed", {
531
531
  desc: item.description.substring(0, 30),
532
532
  error: String(e),
533
- });
533
+ }, "error");
534
534
  }
535
535
  }
536
536
  }
@@ -1059,7 +1059,7 @@ export class UserProfileManager {
1059
1059
  log("ensureArray: failed to parse JSON string, returning []", {
1060
1060
  val: String(val).substring(0, 100),
1061
1061
  error: String(e),
1062
- });
1062
+ }, "error");
1063
1063
  return [];
1064
1064
  }
1065
1065
  }
@@ -1186,7 +1186,7 @@ Answer JSON only: { "duplicate": true|false, "reason": "one sentence explanation
1186
1186
  return result.duplicate || false;
1187
1187
  }
1188
1188
  catch (e) {
1189
- log("profile dedup check: native provider failed", { error: String(e) });
1189
+ log("profile dedup check: native provider failed", { error: String(e) }, "warn");
1190
1190
  }
1191
1191
  }
1192
1192
  if (CONFIG.memoryModel && CONFIG.memoryApiUrl) {
@@ -1221,7 +1221,7 @@ Answer JSON only: { "duplicate": true|false, "reason": "one sentence explanation
1221
1221
  return parsed.duplicate || false;
1222
1222
  }
1223
1223
  catch (e) {
1224
- log("profile dedup check: external API failed", { error: String(e) });
1224
+ log("profile dedup check: external API failed", { error: String(e) }, "error");
1225
1225
  }
1226
1226
  }
1227
1227
  return false;
@@ -1356,7 +1356,7 @@ Answer JSON only: { "conflict": true|false, "reason": "one sentence explanation"
1356
1356
  return result.conflict || false;
1357
1357
  }
1358
1358
  catch (e) {
1359
- log("profile conflict check: native provider failed", { error: String(e) });
1359
+ log("profile conflict check: native provider failed", { error: String(e) }, "warn");
1360
1360
  }
1361
1361
  }
1362
1362
  if (CONFIG.memoryModel && CONFIG.memoryApiUrl) {
@@ -1401,7 +1401,7 @@ Answer JSON only: { "conflict": true|false, "reason": "one sentence explanation"
1401
1401
  return parsed.conflict || false;
1402
1402
  }
1403
1403
  catch (e) {
1404
- log("profile conflict check: external API failed", { error: String(e) });
1404
+ log("profile conflict check: external API failed", { error: String(e) }, "error");
1405
1405
  }
1406
1406
  }
1407
1407
  return false;
@@ -1452,7 +1452,7 @@ Generate a concise, abstract description of the user's general behavioral tenden
1452
1452
  catch (e) {
1453
1453
  log("profile description evolution: native provider failed, trying external API", {
1454
1454
  error: String(e),
1455
- });
1455
+ }, "error");
1456
1456
  }
1457
1457
  }
1458
1458
  if (!newDescription && CONFIG.memoryModel && CONFIG.memoryApiUrl) {
@@ -1460,7 +1460,7 @@ Generate a concise, abstract description of the user's general behavioral tenden
1460
1460
  newDescription = await this.callExternalAPI(systemPrompt, userPrompt);
1461
1461
  }
1462
1462
  catch (e) {
1463
- log("profile description evolution: external API failed", { error: String(e) });
1463
+ log("profile description evolution: external API failed", { error: String(e) }, "error");
1464
1464
  return null;
1465
1465
  }
1466
1466
  }
@@ -1541,7 +1541,7 @@ Generate a concise, abstract description of the user's general behavioral tenden
1541
1541
  else {
1542
1542
  log("profile description evolution: embedding not warmed up, skipping validation", {
1543
1543
  type: itemType,
1544
- });
1544
+ }, "warn");
1545
1545
  }
1546
1546
  const oldDesc = item.description;
1547
1547
  item.description = evolved;
@@ -1555,7 +1555,7 @@ Generate a concise, abstract description of the user's general behavioral tenden
1555
1555
  }
1556
1556
  }
1557
1557
  catch (e) {
1558
- log("profile description evolve error", { type: itemType, error: String(e) });
1558
+ log("profile description evolve error", { type: itemType, error: String(e) }, "error");
1559
1559
  }
1560
1560
  }
1561
1561
  async callHostModel(model, systemPrompt, userPrompt) {
@@ -284,7 +284,7 @@ export function restartWebAutostart(options = {}) {
284
284
  catch (error) {
285
285
  log("OMMS login item restart failed", {
286
286
  code: error instanceof Error ? error.name : "unknown",
287
- });
287
+ }, "error");
288
288
  return false;
289
289
  }
290
290
  }
@@ -301,6 +301,6 @@ export function reconcileWebAutostart(config = CONFIG, options = {}) {
301
301
  catch (error) {
302
302
  log("OMMS login item reconciliation failed", {
303
303
  code: error instanceof Error ? error.name : "unknown",
304
- });
304
+ }, "error");
305
305
  }
306
306
  }
@@ -1,4 +1,5 @@
1
1
  import { routeChat } from "./chat/chat-routes.js";
2
+ import { routeMemoryHosts } from "./memory-host-routes.js";
2
3
  import { routeProfileCleanup } from "./profile-cleanup-routes.js";
3
4
  /**
4
5
  * Route modules that live outside web-server.ts, which sits at its size
@@ -11,5 +12,8 @@ export async function routeModules(path, method, url, req, config) {
11
12
  const cleanup = await routeProfileCleanup(path, method, url, readBody);
12
13
  if (cleanup !== undefined)
13
14
  return { status: 200, body: cleanup };
15
+ const hosts = await routeMemoryHosts(path, method, url, readBody, config);
16
+ if (hosts !== undefined)
17
+ return hosts;
14
18
  return routeChat(path, method, readBody, config);
15
19
  }
@@ -114,7 +114,7 @@ function serveFetch(opts) {
114
114
  const onListening = () => {
115
115
  server.off("error", onError);
116
116
  // Keep a listener, so a later server error is logged instead of crashing the process.
117
- server.on("error", (error) => log("Web server error", { error: String(error) }));
117
+ server.on("error", (error) => log("Web server error", { error: String(error) }, "error"));
118
118
  resolve({
119
119
  stop: () => {
120
120
  server.closeAllConnections();
@@ -271,7 +271,7 @@ export class WebServer {
271
271
  await callback(action);
272
272
  }
273
273
  catch (error) {
274
- log("Power action callback error", { action, error: String(error) });
274
+ log("Power action callback error", { action, error: String(error) }, "error");
275
275
  }
276
276
  finally {
277
277
  this.powerActionRunning = false;
@@ -474,7 +474,7 @@ export class WebServer {
474
474
  await this.onStepAsideCallback();
475
475
  }
476
476
  catch (error) {
477
- log("Step-aside callback error", { error: String(error) });
477
+ log("Step-aside callback error", { error: String(error) }, "error");
478
478
  }
479
479
  return;
480
480
  }
@@ -543,7 +543,7 @@ export class WebServer {
543
543
  catch (error) {
544
544
  log("Importing webServerApiToken failed", {
545
545
  code: error instanceof Error ? error.name : "unknown",
546
- });
546
+ }, "error");
547
547
  }
548
548
  assertWebServerNetworkAuth(this.config.host, tokens.hasUnexpiredApiToken(), this.config.auth?.isEnabled() ?? false);
549
549
  try {
@@ -574,7 +574,7 @@ export class WebServer {
574
574
  else {
575
575
  this.isOwner = false;
576
576
  this.server = null;
577
- log("Web server failed to start", { error: errorMsg });
577
+ log("Web server failed to start", { error: errorMsg }, "error");
578
578
  throw error;
579
579
  }
580
580
  }
@@ -609,7 +609,7 @@ export class WebServer {
609
609
  await this.onStepAsideCallback();
610
610
  }
611
611
  catch (error) {
612
- log("Step-aside callback error", { error: String(error) });
612
+ log("Step-aside callback error", { error: String(error) }, "error");
613
613
  }
614
614
  }
615
615
  /** True when `web update` asked web apps started before this one to retire. */
@@ -666,7 +666,7 @@ export class WebServer {
666
666
  log("Web server port held by a non-responsive process; falling back to next port", {
667
667
  previousPort: this.config.port,
668
668
  newPort: nextPort,
669
- });
669
+ }, "warn");
670
670
  this.config.port = nextPort;
671
671
  }
672
672
  else if (this.config.port >= this.maxFallbackPort &&
@@ -694,7 +694,7 @@ export class WebServer {
694
694
  await this.onTakeoverCallback();
695
695
  }
696
696
  catch (error) {
697
- log("Takeover callback error", { error: String(error) });
697
+ log("Takeover callback error", { error: String(error) }, "error");
698
698
  }
699
699
  }
700
700
  }
@@ -706,12 +706,12 @@ export class WebServer {
706
706
  log("Web server unavailable: every candidate port is held by a non-responsive process", {
707
707
  port: this.config.port,
708
708
  maxFallbackPort: this.maxFallbackPort,
709
- });
709
+ }, "warn");
710
710
  try {
711
711
  this.onPortsExhaustedCallback?.();
712
712
  }
713
713
  catch (error) {
714
- log("Ports exhausted callback error", { error: String(error) });
714
+ log("Ports exhausted callback error", { error: String(error) }, "error");
715
715
  }
716
716
  }
717
717
  async stop() {
@@ -973,7 +973,7 @@ export class WebServer {
973
973
  catch (error) {
974
974
  log("Settings saved, but reloading them failed", {
975
975
  error: error instanceof Error ? error.message : String(error),
976
- });
976
+ }, "error");
977
977
  }
978
978
  if (body.edits.captureRetryRetentionHours === 0) {
979
979
  // Turning the queue off removes waiting turns now, not at the next cleanup.
@@ -987,7 +987,7 @@ export class WebServer {
987
987
  catch (error) {
988
988
  log("Settings saved, but clearing the capture retry queue failed", {
989
989
  code: error instanceof Error ? error.name : "unknown",
990
- });
990
+ }, "error");
991
991
  }
992
992
  }
993
993
  return this.jsonResponse(result);
@@ -1186,7 +1186,7 @@ export class WebServer {
1186
1186
  }
1187
1187
  if (path === "/api/settings/log" && method === "GET") {
1188
1188
  const { readSettingsLog } = await import("./settings-log.js");
1189
- return this.jsonResponse(await readSettingsLog(Number(url.searchParams.get("lines") ?? 200), url.searchParams.get("filter") === "capture"));
1189
+ return this.jsonResponse(await readSettingsLog(Object.fromEntries(url.searchParams)));
1190
1190
  }
1191
1191
  if (path === "/api/tags" && method === "GET") {
1192
1192
  const result = await handleListTags();
@@ -1,3 +1,4 @@
1
+ import type { LogLevel } from "./logger.js";
1
2
  export declare const UPDATE_CHECK_INTERVAL_MS: number;
2
3
  /** An open page triggers a new check once the last one is this old. */
3
4
  export declare const STATUS_RECHECK_AFTER_MS: number;
@@ -23,7 +24,7 @@ export interface WebUpdateDeps {
23
24
  setInterval: (callback: () => void, ms: number) => {
24
25
  unref?: () => void;
25
26
  };
26
- log: (message: string, data: Record<string, unknown>) => void;
27
+ log: (message: string, data: Record<string, unknown>, level?: LogLevel) => void;
27
28
  /** The runtime of this web app; npm must sit beside it. */
28
29
  execPath: string;
29
30
  platform: NodeJS.Platform;
@@ -51,7 +51,7 @@ export class WebUpdate {
51
51
  this.lastCheck = this.deps.now();
52
52
  const latest = await latestNpmVersion(this.deps.fetch);
53
53
  if (latest === null) {
54
- this.deps.log("Web app update check failed", { code: "unreachable" });
54
+ this.deps.log("Web app update check failed", { code: "unreachable" }, "error");
55
55
  return;
56
56
  }
57
57
  this.available = availableUpdate(this.deps.version, latest);
@@ -0,0 +1,3 @@
1
+ import { type WriteHealthResult } from "./write-health.js";
2
+ /** Whether a new memory can be saved now. Reuses a result for 30 seconds unless `fresh`. */
3
+ export declare function checkWriteHealth(fresh?: boolean): Promise<WriteHealthResult>;
@@ -0,0 +1,20 @@
1
+ import { join } from "node:path";
2
+ import { CONFIG } from "../config.js";
3
+ import { createWriteHealth } from "./write-health.js";
4
+ const writeHealth = createWriteHealth({
5
+ openStore: async () => {
6
+ const { tursoConnectionManager } = await import("./turso/connection-manager.js");
7
+ const db = await tursoConnectionManager.getConnection(join(CONFIG.storagePath, "user-prompts.db"));
8
+ await db.all("SELECT 1");
9
+ },
10
+ embed: async () => {
11
+ const { embeddingService } = await import("./embedding.js");
12
+ const vector = await embeddingService.embedWithTimeout("OMMS health check", { task: "query" });
13
+ return vector.length;
14
+ },
15
+ secrets: () => [CONFIG.embeddingApiKey, CONFIG.memoryApiKey],
16
+ });
17
+ /** Whether a new memory can be saved now. Reuses a result for 30 seconds unless `fresh`. */
18
+ export function checkWriteHealth(fresh = false) {
19
+ return writeHealth.check({ fresh });
20
+ }
@@ -0,0 +1,26 @@
1
+ export interface WriteHealthResult {
2
+ ok: boolean;
3
+ /** One short line for the page; null when memories can be saved. */
4
+ reason: string | null;
5
+ checkedAt: number;
6
+ }
7
+ export interface WriteHealthOptions {
8
+ /** Open the memory store and run a trivial query. */
9
+ openStore: () => Promise<void>;
10
+ /** Embed one fixed test string and return the vector length. */
11
+ embed: () => Promise<number>;
12
+ /** Credentials to remove from any reason. */
13
+ secrets: () => Array<string | undefined>;
14
+ now?: () => number;
15
+ }
16
+ /**
17
+ * Whether a new memory can be saved right now: the store opens and the
18
+ * embedding service answers. A result is reused for 30 seconds and one probe
19
+ * serves every caller that starts while it runs, so open tabs cause few
20
+ * embedding calls. The probe sends a fixed string, never conversation text.
21
+ */
22
+ export declare function createWriteHealth(options: WriteHealthOptions): {
23
+ check(request?: {
24
+ fresh?: boolean;
25
+ }): Promise<WriteHealthResult>;
26
+ };
@@ -0,0 +1,58 @@
1
+ import { safeHealthError } from "./safe-health-error.js";
2
+ const CACHE_MS = 30_000;
3
+ const MAX_REASON = 140;
4
+ function embeddingReason(error, secrets) {
5
+ const message = safeHealthError(error, secrets);
6
+ const status = /\b([45]\d{2})\b/.exec(message)?.[1];
7
+ if (status)
8
+ return `Embedding server error (${status})`;
9
+ const line = message.replace(/\s+/g, " ").trim();
10
+ const text = `Embedding failed: ${line}`;
11
+ return text.length > MAX_REASON ? `${text.slice(0, MAX_REASON - 1)}…` : text;
12
+ }
13
+ /**
14
+ * Whether a new memory can be saved right now: the store opens and the
15
+ * embedding service answers. A result is reused for 30 seconds and one probe
16
+ * serves every caller that starts while it runs, so open tabs cause few
17
+ * embedding calls. The probe sends a fixed string, never conversation text.
18
+ */
19
+ export function createWriteHealth(options) {
20
+ const now = options.now ?? Date.now;
21
+ let last = null;
22
+ let running = null;
23
+ async function probe() {
24
+ const secrets = options.secrets();
25
+ let reason = null;
26
+ try {
27
+ await options.openStore();
28
+ }
29
+ catch {
30
+ reason = "Memory store cannot be opened";
31
+ }
32
+ if (reason === null) {
33
+ try {
34
+ const length = await options.embed();
35
+ if (!length)
36
+ reason = "Embedding service returned no values";
37
+ }
38
+ catch (error) {
39
+ reason = embeddingReason(error, secrets);
40
+ }
41
+ }
42
+ return { ok: reason === null, reason, checkedAt: now() };
43
+ }
44
+ return {
45
+ async check(request = {}) {
46
+ if (running)
47
+ return running;
48
+ if (!request.fresh && last && now() - last.checkedAt < CACHE_MS)
49
+ return last;
50
+ running = probe()
51
+ .then((result) => (last = result))
52
+ .finally(() => {
53
+ running = null;
54
+ });
55
+ return running;
56
+ },
57
+ };
58
+ }
@@ -95,7 +95,7 @@ export async function registerV2Adapter(ctx, legacy, memory) {
95
95
  await memory.recordPrompt(sessionID, prompt.messageID, prompt.text);
96
96
  }
97
97
  catch (error) {
98
- log("v2 context: failed to record user prompt", { sessionID, error: String(error) });
98
+ log("v2 context: failed to record user prompt", { sessionID, error: String(error) }, "error");
99
99
  }
100
100
  }
101
101
  if (remaining.length > 0)
@@ -178,7 +178,7 @@ export async function registerV2Adapter(ctx, legacy, memory) {
178
178
  // Start the search now with the entry snapshot, so it overlaps prompt admission.
179
179
  // An edit during retrieval must not change this request's allowance.
180
180
  const promise = memory.retrieve(text, sessionID, budgetBytes).catch((error) => {
181
- log("v2 prompt: memory retrieval failed", { sessionID, error: String(error) });
181
+ log("v2 prompt: memory retrieval failed", { sessionID, error: String(error) }, "error");
182
182
  return null;
183
183
  });
184
184
  setBounded(retrievals, sessionID, { promise, budgetBytes });
@@ -230,7 +230,7 @@ export async function registerV2Adapter(ctx, legacy, memory) {
230
230
  restored.delete(sessionID);
231
231
  }
232
232
  catch (error) {
233
- log("v2 compaction: failed to load session memories", { sessionID, error: String(error) });
233
+ log("v2 compaction: failed to load session memories", { sessionID, error: String(error) }, "error");
234
234
  }
235
235
  });
236
236
  const controller = new AbortController();
@@ -250,7 +250,7 @@ export async function registerV2Adapter(ctx, legacy, memory) {
250
250
  }
251
251
  catch (error) {
252
252
  if (!controller.signal.aborted) {
253
- log("omms v2 event bridge failed", { error: String(error) });
253
+ log("omms v2 event bridge failed", { error: String(error) }, "error");
254
254
  }
255
255
  }
256
256
  })();