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
|
@@ -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 = [];
|
|
@@ -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(
|
|
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
|
|
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,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
|
+
}
|
package/dist/v2/adapter.js
CHANGED
|
@@ -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
|
})();
|