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.
- package/README.md +17 -15
- package/dist/adapters/opencode/auto-capture-summary.js +1 -1
- package/dist/adapters/opencode/import-command.js +2 -2
- package/dist/adapters/opencode/profile-learning.js +9 -9
- package/dist/adapters/opencode/profile-model.js +1 -1
- package/dist/adapters/pi/capture.js +2 -2
- package/dist/adapters/pi/extension.js +8 -8
- package/dist/adapters/pi/import-command.js +2 -2
- package/dist/adapters/pi/live-model.js +1 -1
- package/dist/adapters/pi/profile.js +2 -2
- package/dist/cli/web-power.d.ts +2 -1
- package/dist/cli/web-power.js +2 -2
- package/dist/config.js +1 -1
- package/dist/core/profile-analysis.js +1 -1
- package/dist/core/profile-backoff.js +1 -1
- package/dist/importer/auto-backfill.js +14 -4
- package/dist/importer/backfill-controls.js +1 -1
- package/dist/importer/claude-hook-api.js +5 -5
- package/dist/importer/claude-profile-learning.js +1 -1
- package/dist/importer/import-readiness.d.ts +3 -0
- package/dist/importer/import-readiness.js +11 -4
- package/dist/importer/run-import.js +4 -3
- package/dist/index.js +19 -19
- package/dist/services/ai/providers/anthropic-messages.js +2 -2
- package/dist/services/ai/providers/google-gemini.js +1 -1
- package/dist/services/ai/providers/openai-chat-completion.js +3 -3
- package/dist/services/ai/providers/openai-responses.js +2 -2
- package/dist/services/ai-cleanup-handlers.js +1 -1
- package/dist/services/api-handlers.js +27 -27
- package/dist/services/auto-capture.js +3 -3
- package/dist/services/auto-cleanup-handlers.js +1 -1
- package/dist/services/backfill-state.d.ts +8 -2
- package/dist/services/backfill-state.js +14 -4
- package/dist/services/capture-diagnostics.js +3 -3
- package/dist/services/capture-retry-drain.js +5 -5
- package/dist/services/capture-retry-queue.js +1 -1
- package/dist/services/chat/chat-routes.js +1 -1
- package/dist/services/cleanup-service.js +5 -5
- package/dist/services/client.js +10 -10
- package/dist/services/deduplication-service.js +1 -1
- package/dist/services/embedding.js +2 -2
- package/dist/services/host-appearance.d.ts +28 -0
- package/dist/services/host-appearance.js +117 -0
- package/dist/services/logger.d.ts +7 -1
- package/dist/services/logger.js +9 -3
- package/dist/services/memory-host-routes.d.ts +11 -0
- package/dist/services/memory-host-routes.js +56 -0
- package/dist/services/memory-portability-service.js +1 -1
- package/dist/services/migration-service.js +2 -2
- package/dist/services/settings-log.d.ts +23 -2
- package/dist/services/settings-log.js +65 -7
- package/dist/services/shard-path-migration-service.js +4 -4
- package/dist/services/tag-prefix-migration.js +1 -1
- package/dist/services/turso/connection-manager.js +3 -3
- package/dist/services/turso/keyword-search.js +1 -1
- package/dist/services/turso/legacy-migrator.js +1 -1
- package/dist/services/turso/ready.js +1 -1
- package/dist/services/turso/shard-manager.js +2 -2
- package/dist/services/turso/vector-search.js +3 -3
- package/dist/services/user-profile/ai-cleanup-batches.js +1 -1
- package/dist/services/user-profile/ai-cleanup-job.js +1 -1
- package/dist/services/user-profile/auto-cleanup.js +2 -2
- package/dist/services/user-profile/category-cleanup-job.js +1 -1
- package/dist/services/user-profile/category-cleanup.js +3 -3
- package/dist/services/user-profile/merge-and-save.js +1 -1
- package/dist/services/user-profile/profile-context.js +1 -1
- package/dist/services/user-profile/profile-model.js +1 -1
- package/dist/services/user-profile/user-profile-manager.js +12 -12
- package/dist/services/web-autostart.js +2 -2
- package/dist/services/web-route-modules.js +4 -0
- package/dist/services/web-server.js +13 -13
- package/dist/services/web-update.d.ts +2 -1
- package/dist/services/web-update.js +1 -1
- package/dist/services/write-health-checks.d.ts +3 -0
- package/dist/services/write-health-checks.js +20 -0
- package/dist/services/write-health.d.ts +26 -0
- package/dist/services/write-health.js +58 -0
- package/dist/v2/adapter.js +4 -4
- package/dist/web/assets/index-CIlyLof-.js +126 -0
- package/dist/web/assets/index-DH6qnFfk.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CjfE2byA.js +0 -126
- 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,
|
package/dist/services/client.js
CHANGED
|
@@ -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
|
}
|
|
@@ -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
|
|
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;
|
package/dist/services/logger.js
CHANGED
|
@@ -92,7 +92,12 @@ function ensureLoggerInitialized() {
|
|
|
92
92
|
});
|
|
93
93
|
globalThis[GLOBAL_LOGGER_KEY] = true;
|
|
94
94
|
}
|
|
95
|
-
|
|
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(
|
|
23
|
+
export declare function readSettingsLog(query?: SettingsLogQuery): Promise<{
|
|
3
24
|
path: string;
|
|
4
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
83
|
+
return { path, records: [] };
|
|
25
84
|
const rows = whole.split(/\r?\n/);
|
|
26
85
|
if (rows.at(-1) === "")
|
|
27
86
|
rows.pop();
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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);
|
|
@@ -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);
|