om-memory-system 4.25.1 → 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-CjfE2byA.js +0 -126
  84. package/dist/web/assets/index-SK0gAreR.css +0 -2
@@ -72,7 +72,7 @@ export class CleanupService {
72
72
  }
73
73
  }
74
74
  catch (error) {
75
- log("Cleanup: delete error", { memoryId: memory.id, error: String(error) });
75
+ log("Cleanup: delete error", { memoryId: memory.id, error: String(error) }, "error");
76
76
  }
77
77
  }
78
78
  }
@@ -84,7 +84,7 @@ export class CleanupService {
84
84
  catch (error) {
85
85
  log("Capture attempt cleanup failed", {
86
86
  code: error instanceof Error ? error.name : "unknown",
87
- });
87
+ }, "error");
88
88
  }
89
89
  try {
90
90
  const { pruneCaptureRetries } = await import("./capture-retry-queue.js");
@@ -93,7 +93,7 @@ export class CleanupService {
93
93
  catch (error) {
94
94
  log("Capture retry cleanup failed", {
95
95
  code: error instanceof Error ? error.name : "unknown",
96
- });
96
+ }, "error");
97
97
  }
98
98
  try {
99
99
  const { pruneAutoCleanupRecords } = await import("./user-profile/auto-cleanup-store.js");
@@ -102,14 +102,14 @@ export class CleanupService {
102
102
  catch (error) {
103
103
  log("Automatic cleanup record cleanup failed", {
104
104
  code: error instanceof Error ? error.name : "unknown",
105
- });
105
+ }, "error");
106
106
  }
107
107
  try {
108
108
  await userPromptManager.vacuum();
109
109
  log("Cleanup: VACUUM done", { db: "user-prompts.db" });
110
110
  }
111
111
  catch (err) {
112
- log("Cleanup: VACUUM skipped (DB busy)", { error: String(err) });
112
+ log("Cleanup: VACUUM skipped (DB busy)", { error: String(err) }, "warn");
113
113
  }
114
114
  return {
115
115
  deletedCount: totalDeleted,
@@ -54,7 +54,7 @@ export class LocalMemoryClient {
54
54
  }
55
55
  catch (error) {
56
56
  this.initPromise = null;
57
- log("Turso initialization failed", { error: String(error) });
57
+ log("Turso initialization failed", { error: String(error) }, "error");
58
58
  throw error;
59
59
  }
60
60
  })();
@@ -142,7 +142,7 @@ export class LocalMemoryClient {
142
142
  }
143
143
  catch (error) {
144
144
  const errorMessage = error instanceof Error ? error.message : String(error);
145
- log("searchMemories: error", { error: errorMessage });
145
+ log("searchMemories: error", { error: errorMessage }, "error");
146
146
  return { success: false, error: errorMessage, results: [], total: 0, timing: 0 };
147
147
  }
148
148
  }
@@ -192,7 +192,7 @@ export class LocalMemoryClient {
192
192
  }
193
193
  catch (error) {
194
194
  const errorMessage = error instanceof Error ? error.message : String(error);
195
- log("addMemory: error", { error: errorMessage });
195
+ log("addMemory: error", { error: errorMessage }, "error");
196
196
  return { success: false, error: errorMessage };
197
197
  }
198
198
  }
@@ -215,7 +215,7 @@ export class LocalMemoryClient {
215
215
  }
216
216
  catch (error) {
217
217
  const errorMessage = error instanceof Error ? error.message : String(error);
218
- log("deleteMemory: error", { memoryId, error: errorMessage });
218
+ log("deleteMemory: error", { memoryId, error: errorMessage }, "error");
219
219
  return { success: false, error: errorMessage };
220
220
  }
221
221
  }
@@ -259,7 +259,7 @@ export class LocalMemoryClient {
259
259
  }
260
260
  catch (error) {
261
261
  const errorMessage = error instanceof Error ? error.message : String(error);
262
- log("listMemories: error", { error: errorMessage });
262
+ log("listMemories: error", { error: errorMessage }, "error");
263
263
  return {
264
264
  success: false,
265
265
  error: errorMessage,
@@ -279,7 +279,7 @@ export class LocalMemoryClient {
279
279
  }
280
280
  catch (error) {
281
281
  const errorMessage = error instanceof Error ? error.message : String(error);
282
- log("listShards: error", { error: errorMessage });
282
+ log("listShards: error", { error: errorMessage }, "error");
283
283
  return { success: false, error: errorMessage };
284
284
  }
285
285
  }
@@ -291,7 +291,7 @@ export class LocalMemoryClient {
291
291
  }
292
292
  catch (error) {
293
293
  const errorMessage = error instanceof Error ? error.message : String(error);
294
- log("migrateProjectPath: error", { error: errorMessage });
294
+ log("migrateProjectPath: error", { error: errorMessage }, "error");
295
295
  return { success: false, dryRun: Boolean(options.dryRun), error: errorMessage };
296
296
  }
297
297
  }
@@ -303,7 +303,7 @@ export class LocalMemoryClient {
303
303
  }
304
304
  catch (error) {
305
305
  const errorMessage = error instanceof Error ? error.message : String(error);
306
- log("exportMemories: error", { error: errorMessage });
306
+ log("exportMemories: error", { error: errorMessage }, "error");
307
307
  return { success: false, error: errorMessage };
308
308
  }
309
309
  }
@@ -319,7 +319,7 @@ export class LocalMemoryClient {
319
319
  }
320
320
  catch (error) {
321
321
  const errorMessage = error instanceof Error ? error.message : String(error);
322
- log("importMemories: error", { error: errorMessage });
322
+ log("importMemories: error", { error: errorMessage }, "error");
323
323
  return { success: false, dryRun: Boolean(dryRun), error: errorMessage };
324
324
  }
325
325
  }
@@ -357,7 +357,7 @@ export class LocalMemoryClient {
357
357
  }
358
358
  catch (error) {
359
359
  const errorMessage = error instanceof Error ? error.message : String(error);
360
- log("searchMemoriesBySessionID: error", { error: errorMessage });
360
+ log("searchMemoriesBySessionID: error", { error: errorMessage }, "error");
361
361
  return { success: false, error: errorMessage, results: [], total: 0, timing: 0 };
362
362
  }
363
363
  }
@@ -48,7 +48,7 @@ export class DeduplicationService {
48
48
  log("Deduplication: delete error", {
49
49
  memoryId: dup.id,
50
50
  error: String(error),
51
- });
51
+ }, "error");
52
52
  }
53
53
  }
54
54
  }
@@ -62,7 +62,7 @@ async function ensureTransformersLoaded() {
62
62
  mod.env.backends.onnx.wasm.numThreads = 1;
63
63
  }
64
64
  catch (e) {
65
- log("Failed to set wasm.numThreads", { error: String(e) });
65
+ log("Failed to set wasm.numThreads", { error: String(e) }, "warn");
66
66
  }
67
67
  _transformers = mod;
68
68
  return _transformers;
@@ -183,7 +183,7 @@ export class EmbeddingService {
183
183
  const rewritten = formatOnnxruntimeInitError(error);
184
184
  this.initPromise = null;
185
185
  this.initError = rewritten.message;
186
- log("Failed to initialize embedding model", { error: rewritten.message });
186
+ log("Failed to initialize embedding model", { error: rewritten.message }, "error");
187
187
  throw rewritten;
188
188
  }
189
189
  }
@@ -0,0 +1,28 @@
1
+ export declare const APPEARANCE_HOSTS: readonly ["pi", "opencode", "claude-code"];
2
+ export type AppearanceHost = (typeof APPEARANCE_HOSTS)[number];
3
+ export type HostLook = {
4
+ kind: "default";
5
+ } | {
6
+ kind: "icon";
7
+ icon: string;
8
+ } | {
9
+ kind: "image";
10
+ image: string;
11
+ };
12
+ export type HostLooks = Record<AppearanceHost, HostLook>;
13
+ /** A refused change; `status` is the HTTP status. */
14
+ export declare class HostAppearanceError extends Error {
15
+ readonly status = 400;
16
+ constructor(message: string);
17
+ }
18
+ /**
19
+ * Per-host avatar choices in one global folder. `appearance.json` holds the
20
+ * choices and `<host>.png` holds an uploaded image. Files are written to a
21
+ * temporary name and renamed, so a crash leaves the previous avatar.
22
+ */
23
+ export declare function createHostAppearance(directory: string): {
24
+ read(): Promise<HostLooks>;
25
+ setIcon(host: string, icon: unknown): Promise<void>;
26
+ setImage(host: string, encoded: unknown): Promise<void>;
27
+ reset(host: string): Promise<void>;
28
+ };
@@ -0,0 +1,117 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ export const APPEARANCE_HOSTS = ["pi", "opencode", "claude-code"];
4
+ /** A refused change; `status` is the HTTP status. */
5
+ export class HostAppearanceError extends Error {
6
+ status = 400;
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "HostAppearanceError";
10
+ }
11
+ }
12
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
13
+ const IEND = Buffer.from([0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82]);
14
+ const SIZE = 128;
15
+ const MAX_BYTES = 256 * 1024;
16
+ const ICON_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
17
+ function assertHost(host) {
18
+ if (!APPEARANCE_HOSTS.includes(host)) {
19
+ throw new HostAppearanceError("Unknown host");
20
+ }
21
+ }
22
+ /** Decode and check a host image: a 128 by 128 PNG of at most 256 KB. Nothing else is stored. */
23
+ function decodeImage(encoded) {
24
+ if (typeof encoded !== "string" || encoded.length === 0 || encoded.length > MAX_BYTES * 2) {
25
+ throw new HostAppearanceError("The image must be a 128 by 128 PNG of at most 256 KB");
26
+ }
27
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded))
28
+ throw new HostAppearanceError("Invalid image data");
29
+ const bytes = Buffer.from(encoded, "base64");
30
+ const isPng = bytes.length >= 45 &&
31
+ bytes.length <= MAX_BYTES &&
32
+ bytes.subarray(0, 8).equals(PNG_SIGNATURE) &&
33
+ bytes.readUInt32BE(8) === 13 &&
34
+ bytes.subarray(12, 16).toString("ascii") === "IHDR" &&
35
+ bytes.subarray(bytes.length - 8).equals(IEND);
36
+ if (!isPng || bytes.readUInt32BE(16) !== SIZE || bytes.readUInt32BE(20) !== SIZE) {
37
+ throw new HostAppearanceError("The image must be a 128 by 128 PNG of at most 256 KB");
38
+ }
39
+ return bytes;
40
+ }
41
+ /**
42
+ * Per-host avatar choices in one global folder. `appearance.json` holds the
43
+ * choices and `<host>.png` holds an uploaded image. Files are written to a
44
+ * temporary name and renamed, so a crash leaves the previous avatar.
45
+ */
46
+ export function createHostAppearance(directory) {
47
+ const settingsPath = join(directory, "appearance.json");
48
+ const imagePath = (host) => join(directory, `${host}.png`);
49
+ async function readSaved() {
50
+ try {
51
+ const parsed = JSON.parse(await readFile(settingsPath, "utf8"));
52
+ return parsed && typeof parsed === "object" ? parsed : {};
53
+ }
54
+ catch {
55
+ return {};
56
+ }
57
+ }
58
+ async function atomicWrite(path, data) {
59
+ await mkdir(directory, { recursive: true });
60
+ const temporary = `${path}.${process.pid}.tmp`;
61
+ await writeFile(temporary, data);
62
+ await rename(temporary, path);
63
+ }
64
+ async function save(host, value) {
65
+ const saved = await readSaved();
66
+ if (value)
67
+ saved[host] = value;
68
+ else
69
+ delete saved[host];
70
+ await atomicWrite(settingsPath, JSON.stringify(saved));
71
+ }
72
+ return {
73
+ async read() {
74
+ const saved = await readSaved();
75
+ const looks = {};
76
+ for (const host of APPEARANCE_HOSTS) {
77
+ const entry = saved[host];
78
+ looks[host] = { kind: "default" };
79
+ if (entry?.kind === "icon" && ICON_NAME.test(String(entry.icon))) {
80
+ looks[host] = { kind: "icon", icon: entry.icon };
81
+ }
82
+ else if (entry?.kind === "image") {
83
+ try {
84
+ const bytes = await readFile(imagePath(host));
85
+ looks[host] = {
86
+ kind: "image",
87
+ image: `data:image/png;base64,${bytes.toString("base64")}`,
88
+ };
89
+ }
90
+ catch {
91
+ // A missing image file shows the default mark.
92
+ }
93
+ }
94
+ }
95
+ return looks;
96
+ },
97
+ async setIcon(host, icon) {
98
+ assertHost(host);
99
+ if (typeof icon !== "string" || icon.length > 40 || !ICON_NAME.test(icon)) {
100
+ throw new HostAppearanceError("Invalid icon name");
101
+ }
102
+ await save(host, { kind: "icon", icon });
103
+ await rm(imagePath(host), { force: true });
104
+ },
105
+ async setImage(host, encoded) {
106
+ assertHost(host);
107
+ const bytes = decodeImage(encoded);
108
+ await atomicWrite(imagePath(host), bytes);
109
+ await save(host, { kind: "image" });
110
+ },
111
+ async reset(host) {
112
+ assertHost(host);
113
+ await save(host, undefined);
114
+ await rm(imagePath(host), { force: true });
115
+ },
116
+ };
117
+ }
@@ -1 +1,7 @@
1
- export declare function log(message: string, data?: unknown): void;
1
+ export type LogLevel = "warn" | "error";
2
+ /**
3
+ * Writes one line: `[time] LEVEL message: {data}`. No level means INFO. The
4
+ * level is an extra argument, not a method, because many tests stub this module
5
+ * with a bare `log`.
6
+ */
7
+ export declare function log(message: string, data?: unknown, level?: LogLevel): void;
@@ -92,7 +92,12 @@ function ensureLoggerInitialized() {
92
92
  });
93
93
  globalThis[GLOBAL_LOGGER_KEY] = true;
94
94
  }
95
- export function log(message, data) {
95
+ /**
96
+ * Writes one line: `[time] LEVEL message: {data}`. No level means INFO. The
97
+ * level is an extra argument, not a method, because many tests stub this module
98
+ * with a bare `log`.
99
+ */
100
+ export function log(message, data, level) {
96
101
  ensureLoggerInitialized();
97
102
  const today = formatDate(new Date());
98
103
  if (globalThis[LAST_ROTATE_DATE_KEY] !== today) {
@@ -101,8 +106,9 @@ export function log(message, data) {
101
106
  }
102
107
  const logFile = getLogFilePath();
103
108
  const timestamp = formatTimestamp(new Date());
109
+ const token = level ? level.toUpperCase() : "INFO";
104
110
  const line = data
105
- ? `[${timestamp}] ${message}: ${JSON.stringify(data)}\n`
106
- : `[${timestamp}] ${message}\n`;
111
+ ? `[${timestamp}] ${token} ${message}: ${JSON.stringify(data)}\n`
112
+ : `[${timestamp}] ${token} ${message}\n`;
107
113
  appendFileSync(logFile, line);
108
114
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Routes for the Memory page: whether memories can be saved, and each host's
3
+ * avatar. Returns undefined for paths it does not own. The web token check
4
+ * runs before this, for every `/api/` path.
5
+ */
6
+ export declare function routeMemoryHosts(path: string, method: string, url: URL, readBody: () => Promise<any>, config: {
7
+ storagePath: string;
8
+ }): Promise<{
9
+ status: number;
10
+ body: unknown;
11
+ } | undefined>;
@@ -0,0 +1,56 @@
1
+ import { join } from "node:path";
2
+ import { HostAppearanceError, createHostAppearance } from "./host-appearance.js";
3
+ const instances = new Map();
4
+ function appearanceFor(storagePath) {
5
+ const directory = join(storagePath, "host-appearance");
6
+ let appearance = instances.get(directory);
7
+ if (!appearance) {
8
+ appearance = createHostAppearance(directory);
9
+ instances.set(directory, appearance);
10
+ }
11
+ return appearance;
12
+ }
13
+ const refused = (error) => {
14
+ if (error instanceof HostAppearanceError) {
15
+ return { status: error.status, body: { success: false, error: error.message } };
16
+ }
17
+ throw error;
18
+ };
19
+ /**
20
+ * Routes for the Memory page: whether memories can be saved, and each host's
21
+ * avatar. Returns undefined for paths it does not own. The web token check
22
+ * runs before this, for every `/api/` path.
23
+ */
24
+ export async function routeMemoryHosts(path, method, url, readBody, config) {
25
+ if (path === "/api/memory/write-health" && method === "GET") {
26
+ const { checkWriteHealth } = await import("./write-health-checks.js");
27
+ return { status: 200, body: await checkWriteHealth(url.searchParams.get("fresh") === "1") };
28
+ }
29
+ if (path === "/api/host-appearance" && method === "GET") {
30
+ return { status: 200, body: { hosts: await appearanceFor(config.storagePath).read() } };
31
+ }
32
+ const host = /^\/api\/host-appearance\/([^/]+)$/.exec(path)?.[1];
33
+ if (!host)
34
+ return undefined;
35
+ const appearance = appearanceFor(config.storagePath);
36
+ try {
37
+ if (method === "PUT") {
38
+ const body = await readBody();
39
+ if (body?.kind === "icon")
40
+ await appearance.setIcon(host, body.icon);
41
+ else if (body?.kind === "image")
42
+ await appearance.setImage(host, body.png);
43
+ else
44
+ throw new HostAppearanceError("Choose an icon or an image");
45
+ return { status: 200, body: { hosts: await appearance.read() } };
46
+ }
47
+ if (method === "DELETE") {
48
+ await appearance.reset(host);
49
+ return { status: 200, body: { hosts: await appearance.read() } };
50
+ }
51
+ }
52
+ catch (error) {
53
+ return refused(error);
54
+ }
55
+ return undefined;
56
+ }
@@ -321,7 +321,7 @@ export class MemoryPortabilityService {
321
321
  }
322
322
  }
323
323
  catch (cleanupError) {
324
- log("Memory import cleanup failed", { error: String(cleanupError) });
324
+ log("Memory import cleanup failed", { error: String(cleanupError) }, "error");
325
325
  }
326
326
  }
327
327
  return {
@@ -46,7 +46,7 @@ export class MigrationService {
46
46
  log("Migration: error checking shard", {
47
47
  shardId: shard.id,
48
48
  error: String(error),
49
- });
49
+ }, "error");
50
50
  }
51
51
  }
52
52
  return {
@@ -82,7 +82,7 @@ export class MigrationService {
82
82
  return await this.reEmbedMigration(mismatch, startTime);
83
83
  }
84
84
  catch (error) {
85
- log("Migration: failed", { error: String(error) });
85
+ log("Migration: failed", { error: String(error) }, "error");
86
86
  return {
87
87
  success: false,
88
88
  strategy,
@@ -1,5 +1,26 @@
1
+ export type LogRecordLevel = "info" | "warn" | "error";
2
+ export interface LogRecord {
3
+ /** Line number of the record's first line within the tail that was read. */
4
+ seq: number;
5
+ time: string;
6
+ level: LogRecordLevel;
7
+ /** True when the level was guessed from the message, as for lines written before levels. */
8
+ guessed: boolean;
9
+ message: string;
10
+ /** The JSON text after the message, or null when the line has none. */
11
+ data: string | null;
12
+ }
13
+ export interface SettingsLogQuery {
14
+ lines?: number | string;
15
+ /** `capture` keeps only `Capture attempt` records. */
16
+ filter?: string;
17
+ /** `warn` keeps warn and error records, `error` keeps error records. */
18
+ level?: string;
19
+ }
20
+ /** Guess the level of a line written before levels, from its message text only. */
21
+ export declare function guessLevel(message: string): LogRecordLevel;
1
22
  /** Read the bounded tail of OMMS's own log. The request cannot choose a path. */
2
- export declare function readSettingsLog(lines?: number, captureOnly?: boolean): Promise<{
23
+ export declare function readSettingsLog(query?: SettingsLogQuery): Promise<{
3
24
  path: string;
4
- lines: string[];
25
+ records: LogRecord[];
5
26
  }>;
@@ -1,16 +1,75 @@
1
1
  import { open, stat } from "node:fs/promises";
2
2
  import { getLogFilePath } from "./log-path.js";
3
+ const LEVEL_RANK = { info: 0, warn: 1, error: 2 };
4
+ const RECORD_LINE = /^\[([^\]]+)\] (?:(INFO|WARN|ERROR) )?(.*)$/;
5
+ const SESSION_LINE = /^--- Session started: (.+) ---$/;
6
+ /** Guess the level of a line written before levels, from its message text only. */
7
+ export function guessLevel(message) {
8
+ if (/fail|error|exception/i.test(message))
9
+ return "error";
10
+ if (/warn|fallback|falling back|unavailable|skip|retry/i.test(message))
11
+ return "warn";
12
+ return "info";
13
+ }
14
+ function splitData(text) {
15
+ const at = [text.indexOf(": {"), text.indexOf(": [")].filter((i) => i >= 0);
16
+ if (at.length === 0)
17
+ return { message: text, data: null };
18
+ const i = Math.min(...at);
19
+ return { message: text.slice(0, i), data: text.slice(i + 2) };
20
+ }
21
+ function parseRecords(rows) {
22
+ const records = [];
23
+ rows.forEach((row, seq) => {
24
+ if (row === "")
25
+ return;
26
+ const session = SESSION_LINE.exec(row);
27
+ if (session) {
28
+ records.push({
29
+ seq,
30
+ time: session[1],
31
+ level: "info",
32
+ guessed: false,
33
+ message: "Session started",
34
+ data: null,
35
+ });
36
+ return;
37
+ }
38
+ const match = RECORD_LINE.exec(row);
39
+ if (!match) {
40
+ const last = records.at(-1);
41
+ if (last)
42
+ last.message += `\n${row}`;
43
+ else
44
+ records.push({ seq, time: "", level: "info", guessed: true, message: row, data: null });
45
+ return;
46
+ }
47
+ const { message, data } = splitData(match[3]);
48
+ const token = match[2];
49
+ records.push({
50
+ seq,
51
+ time: match[1],
52
+ level: token ? token.toLowerCase() : guessLevel(message),
53
+ guessed: !token,
54
+ message,
55
+ data,
56
+ });
57
+ });
58
+ return records;
59
+ }
3
60
  /** Read the bounded tail of OMMS's own log. The request cannot choose a path. */
4
- export async function readSettingsLog(lines = 200, captureOnly = false) {
61
+ export async function readSettingsLog(query = {}) {
5
62
  const path = getLogFilePath();
63
+ const lines = Number(query.lines ?? 200);
6
64
  const count = Number.isFinite(lines) ? Math.max(1, Math.min(2000, Math.floor(lines))) : 200;
65
+ const minimum = query.level === "warn" || query.level === "error" ? query.level : "info";
7
66
  let size;
8
67
  try {
9
68
  size = (await stat(path)).size;
10
69
  }
11
70
  catch (error) {
12
71
  if (error.code === "ENOENT")
13
- return { path, lines: [] };
72
+ return { path, records: [] };
14
73
  throw error;
15
74
  }
16
75
  const start = Math.max(0, size - 256 * 1024);
@@ -21,14 +80,13 @@ export async function readSettingsLog(lines = 200, captureOnly = false) {
21
80
  const text = buffer.subarray(0, bytesRead).toString("utf8");
22
81
  const whole = start === 0 ? text : text.slice(text.indexOf("\n") + 1);
23
82
  if (start > 0 && !text.includes("\n"))
24
- return { path, lines: [] };
83
+ return { path, records: [] };
25
84
  const rows = whole.split(/\r?\n/);
26
85
  if (rows.at(-1) === "")
27
86
  rows.pop();
28
- return {
29
- path,
30
- lines: rows.filter((line) => !captureOnly || line.includes("Capture attempt")).slice(-count),
31
- };
87
+ const records = parseRecords(rows).filter((record) => LEVEL_RANK[record.level] >= LEVEL_RANK[minimum] &&
88
+ (query.filter !== "capture" || record.message.includes("Capture attempt")));
89
+ return { path, records: records.slice(-count) };
32
90
  }
33
91
  finally {
34
92
  await file.close();
@@ -240,7 +240,7 @@ export class ShardPathMigrationService {
240
240
  await userPromptManager.updateProjectPath(source.group.projectPath, target.projectPath);
241
241
  }
242
242
  catch (error) {
243
- log("Path migration prompt metadata update failed", { error: String(error) });
243
+ log("Path migration prompt metadata update failed", { error: String(error) }, "error");
244
244
  }
245
245
  }
246
246
  log("Path migration completed", {
@@ -289,7 +289,7 @@ export class ShardPathMigrationService {
289
289
  }
290
290
  catch (error) {
291
291
  rollbackFailed = true;
292
- log("Path migration source rollback failed", { move, error: String(error) });
292
+ log("Path migration source rollback failed", { move, error: String(error) }, "error");
293
293
  }
294
294
  }
295
295
  for (const archived of state.archivedTargets) {
@@ -302,7 +302,7 @@ export class ShardPathMigrationService {
302
302
  }
303
303
  catch (error) {
304
304
  rollbackFailed = true;
305
- log("Path migration target rollback failed", { archived, error: String(error) });
305
+ log("Path migration target rollback failed", { archived, error: String(error) }, "error");
306
306
  }
307
307
  }
308
308
  // Startup reconciliation may have registered a newly renamed shard before
@@ -317,7 +317,7 @@ export class ShardPathMigrationService {
317
317
  }
318
318
  catch (error) {
319
319
  rollbackFailed = true;
320
- log("Path migration stale target cleanup failed", { error: String(error) });
320
+ log("Path migration stale target cleanup failed", { error: String(error) }, "error");
321
321
  }
322
322
  if (!rollbackFailed && existsSync(statePath)) {
323
323
  unlinkSync(statePath);
@@ -434,6 +434,6 @@ export async function runStartupTagPrefixGate() {
434
434
  }
435
435
  const warning = explicitOpencodePrefixWarning(getExplicitContainerTagPrefix());
436
436
  if (warning) {
437
- log(warning);
437
+ log(warning, undefined, "warn");
438
438
  }
439
439
  }
@@ -111,7 +111,7 @@ export class TursoConnectionManager {
111
111
  await db.close();
112
112
  }
113
113
  catch (error) {
114
- log("Error closing Turso database", { path: dbPath, error: String(error) });
114
+ log("Error closing Turso database", { path: dbPath, error: String(error) }, "error");
115
115
  }
116
116
  this.connections.delete(dbPath);
117
117
  }
@@ -133,7 +133,7 @@ export class TursoConnectionManager {
133
133
  await db.close();
134
134
  }
135
135
  catch (error) {
136
- log("Error closing Turso database", { path, error: String(error) });
136
+ log("Error closing Turso database", { path, error: String(error) }, "error");
137
137
  }
138
138
  }
139
139
  this.connections.clear();
@@ -153,7 +153,7 @@ export class TursoConnectionManager {
153
153
  db.getClient().close();
154
154
  }
155
155
  catch (error) {
156
- log("Error closing Turso database (sync)", { path, error: String(error) });
156
+ log("Error closing Turso database (sync)", { path, error: String(error) }, "error");
157
157
  }
158
158
  }
159
159
  this.connections.clear();
@@ -138,7 +138,7 @@ export async function searchKeywords(db, queryText, containerTag, limit) {
138
138
  log("Keyword search failed; using vector results", {
139
139
  code: "keyword_index_failed",
140
140
  error: error instanceof Error ? error.name : "unknown",
141
- });
141
+ }, "warn");
142
142
  return null;
143
143
  }
144
144
  }
@@ -303,7 +303,7 @@ async function migrateMemoryShard(dbPath) {
303
303
  const record = rowToRecord(row);
304
304
  if (!record) {
305
305
  skippedIds.push(String(row.id));
306
- log("Legacy migration skipped memory without vector", { memoryId: row.id, dbPath });
306
+ log("Legacy migration skipped memory without vector", { memoryId: row.id, dbPath }, "warn");
307
307
  continue;
308
308
  }
309
309
  records.push(record);