@codemem/server 0.25.3 → 0.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/dist/index.js +105 -23
- package/dist/index.js.map +1 -1
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/package.json +2 -2
- package/static/app.js +11698 -10023
- package/static/index.html +212 -11
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, MemoryStore, VERSION, VERSION as VERSION$1, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, cleanupNonces, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorStatusSnapshot, createCoordinatorReciprocalApproval, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listMaintenanceJobs, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
3
|
+
import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, MemoryStore, VERSION, VERSION as VERSION$1, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, cleanupNonces, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorStatusSnapshot, createCoordinatorReciprocalApproval, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listMaintenanceJobs, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
4
4
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import { createMiddleware } from "hono/factory";
|
|
@@ -553,6 +553,39 @@ function queryBool(value) {
|
|
|
553
553
|
}
|
|
554
554
|
//#endregion
|
|
555
555
|
//#region src/routes/memory.ts
|
|
556
|
+
function cleanOwnershipValue(value) {
|
|
557
|
+
if (typeof value !== "string") return null;
|
|
558
|
+
const trimmed = value.trim();
|
|
559
|
+
return trimmed ? trimmed : null;
|
|
560
|
+
}
|
|
561
|
+
function buildOwnershipContext(store) {
|
|
562
|
+
const claimedPeerIds = new Set(store.sameActorPeerIds());
|
|
563
|
+
return {
|
|
564
|
+
actorId: store.actorId,
|
|
565
|
+
claimedPeerIds,
|
|
566
|
+
deviceId: store.deviceId,
|
|
567
|
+
legacyActorIds: new Set(Array.from(claimedPeerIds, (peerId) => `legacy-sync:${peerId}`))
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function memoryOwnedBySelf(row, ownership, metadata = fromJson(row.metadata_json ?? null)) {
|
|
571
|
+
const meta = metadata;
|
|
572
|
+
const actorId = cleanOwnershipValue(row.actor_id) ?? cleanOwnershipValue(meta.actor_id);
|
|
573
|
+
if (actorId === ownership.actorId) return true;
|
|
574
|
+
const deviceId = cleanOwnershipValue(row.origin_device_id) ?? cleanOwnershipValue(meta.origin_device_id);
|
|
575
|
+
if (deviceId === ownership.deviceId) return true;
|
|
576
|
+
if (deviceId && ownership.claimedPeerIds.has(deviceId)) return true;
|
|
577
|
+
if (actorId && ownership.legacyActorIds.has(actorId)) return true;
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
function serializeMemoryRow(ownership, row) {
|
|
581
|
+
const metadata = fromJson(row.metadata_json ?? null);
|
|
582
|
+
return {
|
|
583
|
+
...row,
|
|
584
|
+
kind: canonicalMemoryKind(row.kind ?? null, row.metadata_json),
|
|
585
|
+
metadata_json: metadata,
|
|
586
|
+
owned_by_self: memoryOwnedBySelf(row, ownership, metadata)
|
|
587
|
+
};
|
|
588
|
+
}
|
|
556
589
|
/**
|
|
557
590
|
* Attach session project/cwd fields to memory items.
|
|
558
591
|
*/
|
|
@@ -615,14 +648,12 @@ function queryMemoryPage(store, options) {
|
|
|
615
648
|
});
|
|
616
649
|
const where = ["memory_items.active = 1", ...filterResult.clauses].join(" AND ");
|
|
617
650
|
const from = filterResult.joinSessions ? "memory_items JOIN sessions ON sessions.id = memory_items.session_id" : "memory_items";
|
|
618
|
-
|
|
651
|
+
const rows = store.db.prepare(`SELECT memory_items.* FROM ${from}
|
|
619
652
|
WHERE ${where}
|
|
620
653
|
ORDER BY memory_items.created_at DESC
|
|
621
|
-
LIMIT ? OFFSET ?`).all(...filterResult.params, options.limit + 1, options.offset)
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
metadata_json: fromJson(row.metadata_json ?? null)
|
|
625
|
-
}));
|
|
654
|
+
LIMIT ? OFFSET ?`).all(...filterResult.params, options.limit + 1, options.offset);
|
|
655
|
+
const ownership = buildOwnershipContext(store);
|
|
656
|
+
return rows.map((row) => serializeMemoryRow(ownership, row));
|
|
626
657
|
}
|
|
627
658
|
function isSummaryLikeMemory$1(item) {
|
|
628
659
|
return isSummaryLikeMemory({
|
|
@@ -870,6 +901,31 @@ function memoryRoutes(getStore) {
|
|
|
870
901
|
return c.json({ error: msg }, 400);
|
|
871
902
|
}
|
|
872
903
|
});
|
|
904
|
+
app.post("/api/memories/forget", async (c) => {
|
|
905
|
+
const store = getStore();
|
|
906
|
+
let body;
|
|
907
|
+
try {
|
|
908
|
+
body = await c.req.json();
|
|
909
|
+
} catch {
|
|
910
|
+
return c.json({ error: "invalid JSON" }, 400);
|
|
911
|
+
}
|
|
912
|
+
const memoryId = parseStrictInteger(typeof body.memory_id === "string" ? body.memory_id : String(body.memory_id ?? ""));
|
|
913
|
+
if (memoryId == null || memoryId <= 0) return c.json({ error: "memory_id must be int" }, 400);
|
|
914
|
+
const row = drizzle(store.db, { schema }).select().from(schema.memoryItems).where(eq(schema.memoryItems.id, memoryId)).get();
|
|
915
|
+
if (!row) return c.json({ error: "memory not found" }, 404);
|
|
916
|
+
if (!memoryOwnedBySelf(row, buildOwnershipContext(store))) return c.json({ error: "memory not owned by this device" }, 403);
|
|
917
|
+
if (Number(row.active ?? 1) === 0 || row.deleted_at != null) return c.json({ status: "ok" });
|
|
918
|
+
try {
|
|
919
|
+
store.forget(memoryId);
|
|
920
|
+
return c.json({ status: "ok" });
|
|
921
|
+
} catch (err) {
|
|
922
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
923
|
+
if (msg.includes("not found")) return c.json({ error: msg }, 404);
|
|
924
|
+
if (msg.includes("not owned")) return c.json({ error: msg }, 403);
|
|
925
|
+
if (msg.includes("sync_rebootstrap_in_progress")) return c.json({ error: msg }, 409);
|
|
926
|
+
return c.json({ error: msg }, 400);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
873
929
|
return app;
|
|
874
930
|
}
|
|
875
931
|
//#endregion
|
|
@@ -1387,6 +1443,19 @@ function summarizeMaintenanceJobs(jobs, showDiagnostics) {
|
|
|
1387
1443
|
} : {}
|
|
1388
1444
|
}));
|
|
1389
1445
|
}
|
|
1446
|
+
function redactSemanticIndexDiagnostics(diagnostics, showDiagnostics) {
|
|
1447
|
+
if (showDiagnostics || !diagnostics.maintenance_job) return diagnostics;
|
|
1448
|
+
const summary = diagnostics.state === "pending" ? "Semantic-index catch-up is pending" : diagnostics.state === "failed" ? "Semantic-index catch-up failed" : diagnostics.state === "degraded" ? "Semantic index is running in degraded mode" : diagnostics.summary;
|
|
1449
|
+
return {
|
|
1450
|
+
...diagnostics,
|
|
1451
|
+
summary,
|
|
1452
|
+
maintenance_job: {
|
|
1453
|
+
...diagnostics.maintenance_job,
|
|
1454
|
+
message: null,
|
|
1455
|
+
error: null
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1390
1459
|
function syncKeysDir() {
|
|
1391
1460
|
return process.env.CODEMEM_KEYS_DIR?.trim() || void 0;
|
|
1392
1461
|
}
|
|
@@ -2203,29 +2272,40 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2203
2272
|
app.get("/api/sync/status", async (c) => {
|
|
2204
2273
|
const store = getStore();
|
|
2205
2274
|
{
|
|
2275
|
+
const traceSync = (label, fn) => {
|
|
2276
|
+
if (process.env.CODEMEM_TRACE_SYNC_STATUS !== "1") return fn();
|
|
2277
|
+
const startedAt = Date.now();
|
|
2278
|
+
console.warn(`[codemem sync-status] ${label} start`);
|
|
2279
|
+
try {
|
|
2280
|
+
return fn();
|
|
2281
|
+
} finally {
|
|
2282
|
+
console.warn(`[codemem sync-status] ${label} ${Date.now() - startedAt}ms`);
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2206
2285
|
const showDiag = queryBool(c.req.query("includeDiagnostics"));
|
|
2207
2286
|
const includeJoinRequests = queryBool(c.req.query("includeJoinRequests"));
|
|
2208
2287
|
const project = c.req.query("project") || null;
|
|
2209
|
-
const config = readCoordinatorSyncConfig();
|
|
2210
|
-
const syncReset = getSyncResetState(store.db);
|
|
2288
|
+
const config = traceSync("readCoordinatorSyncConfig", () => readCoordinatorSyncConfig());
|
|
2289
|
+
const syncReset = traceSync("getSyncResetState", () => getSyncResetState(store.db));
|
|
2211
2290
|
const d = drizzle(store.db, { schema });
|
|
2212
|
-
const deviceRow = d.select({
|
|
2291
|
+
const deviceRow = traceSync("deviceRow", () => d.select({
|
|
2213
2292
|
device_id: schema.syncDevice.device_id,
|
|
2214
2293
|
fingerprint: schema.syncDevice.fingerprint
|
|
2215
|
-
}).from(schema.syncDevice).limit(1).get();
|
|
2216
|
-
const daemonState = d.select().from(schema.syncDaemonState).where(eq(schema.syncDaemonState.id, 1)).get();
|
|
2217
|
-
const peerCountRow = d.select({ total: count() }).from(schema.syncPeers).get();
|
|
2294
|
+
}).from(schema.syncDevice).limit(1).get());
|
|
2295
|
+
const daemonState = traceSync("daemonState", () => d.select().from(schema.syncDaemonState).where(eq(schema.syncDaemonState.id, 1)).get());
|
|
2296
|
+
const peerCountRow = traceSync("peerCountRow", () => d.select({ total: count() }).from(schema.syncPeers).get());
|
|
2218
2297
|
let retentionState;
|
|
2219
2298
|
try {
|
|
2220
|
-
retentionState = d.select().from(schema.syncRetentionState).where(eq(schema.syncRetentionState.id, 1)).get();
|
|
2299
|
+
retentionState = traceSync("retentionState", () => d.select().from(schema.syncRetentionState).where(eq(schema.syncRetentionState.id, 1)).get());
|
|
2221
2300
|
} catch {
|
|
2222
2301
|
retentionState = void 0;
|
|
2223
2302
|
}
|
|
2224
|
-
const lastSyncRow = d.select({ last_sync_at: max(schema.syncPeers.last_sync_at) }).from(schema.syncPeers).get();
|
|
2303
|
+
const lastSyncRow = traceSync("lastSyncRow", () => d.select({ last_sync_at: max(schema.syncPeers.last_sync_at) }).from(schema.syncPeers).get());
|
|
2304
|
+
const semanticIndex = traceSync("semanticIndex", () => redactSemanticIndexDiagnostics(getSemanticIndexDiagnostics(store.db, { fastCounts: !showDiag }), showDiag));
|
|
2225
2305
|
const lastError = daemonState?.last_error;
|
|
2226
2306
|
const lastErrorAt = daemonState?.last_error_at;
|
|
2227
2307
|
const lastOkAt = daemonState?.last_ok_at;
|
|
2228
|
-
const viewerBinding = readViewerBinding(store.dbPath);
|
|
2308
|
+
const viewerBinding = traceSync("readViewerBinding", () => readViewerBinding(store.dbPath));
|
|
2229
2309
|
const daemonRunning = viewerBinding ? await portOpen(viewerBinding.host, viewerBinding.port) : false;
|
|
2230
2310
|
const daemonDetail = viewerBinding ? daemonRunning ? `viewer pidfile at ${viewerBinding.host}:${viewerBinding.port}` : `pidfile present but ${viewerBinding.host}:${viewerBinding.port} is unreachable` : null;
|
|
2231
2311
|
let daemonStateValue = "ok";
|
|
@@ -2248,6 +2328,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2248
2328
|
last_error: retentionState?.last_error ?? null,
|
|
2249
2329
|
last_error_at: retentionState?.last_error_at ?? null
|
|
2250
2330
|
},
|
|
2331
|
+
semantic_index: semanticIndex,
|
|
2251
2332
|
peer_count: Number(peerCountRow?.total ?? 0),
|
|
2252
2333
|
last_sync_at: lastSyncRow?.last_sync_at ?? null,
|
|
2253
2334
|
daemon_state: daemonStateValue,
|
|
@@ -2275,14 +2356,14 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2275
2356
|
statusPayload.daemon_running = runtimeStatus.phase === "starting" || daemonRunning;
|
|
2276
2357
|
statusPayload.daemon_detail = runtimeStatus.detail ?? daemonDetail;
|
|
2277
2358
|
}
|
|
2278
|
-
const peersItems = store.db.prepare(PEERS_QUERY).all().map((row) => {
|
|
2359
|
+
const peersItems = traceSync("peerRows", () => store.db.prepare(PEERS_QUERY).all()).map((row) => {
|
|
2279
2360
|
const peer = mapPeerRow(store, row, showDiag);
|
|
2280
2361
|
peer.status = peerStatus(peer);
|
|
2281
2362
|
return peer;
|
|
2282
2363
|
});
|
|
2283
2364
|
const peersMap = {};
|
|
2284
2365
|
for (const peer of peersItems) peersMap[String(peer.peer_device_id)] = peer.status;
|
|
2285
|
-
const attemptRows = d.select({
|
|
2366
|
+
const attemptRows = traceSync("attemptRows", () => d.select({
|
|
2286
2367
|
peer_device_id: schema.syncAttempts.peer_device_id,
|
|
2287
2368
|
ok: schema.syncAttempts.ok,
|
|
2288
2369
|
error: schema.syncAttempts.error,
|
|
@@ -2290,9 +2371,9 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2290
2371
|
finished_at: schema.syncAttempts.finished_at,
|
|
2291
2372
|
ops_in: schema.syncAttempts.ops_in,
|
|
2292
2373
|
ops_out: schema.syncAttempts.ops_out
|
|
2293
|
-
}).from(schema.syncAttempts).orderBy(desc(schema.syncAttempts.finished_at)).limit(25).all();
|
|
2374
|
+
}).from(schema.syncAttempts).orderBy(desc(schema.syncAttempts.finished_at)).limit(25).all());
|
|
2294
2375
|
const peerAddressMap = /* @__PURE__ */ new Map();
|
|
2295
|
-
store.db.prepare("SELECT peer_device_id, addresses_json FROM sync_peers").all().forEach((peerRow) => {
|
|
2376
|
+
traceSync("peerAddressRows", () => store.db.prepare("SELECT peer_device_id, addresses_json FROM sync_peers").all()).forEach((peerRow) => {
|
|
2296
2377
|
const addrs = safeJsonList(peerRow.addresses_json);
|
|
2297
2378
|
if (addrs.length) peerAddressMap.set(String(peerRow.peer_device_id ?? ""), addrs);
|
|
2298
2379
|
});
|
|
@@ -2313,9 +2394,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2313
2394
|
sync: {},
|
|
2314
2395
|
ping: {}
|
|
2315
2396
|
};
|
|
2316
|
-
const legacyDevices = store.claimableLegacyDeviceIds();
|
|
2317
|
-
const sharingReview = store.sharingReviewSummary(project);
|
|
2318
|
-
const
|
|
2397
|
+
const legacyDevices = traceSync("legacyDevices", () => store.claimableLegacyDeviceIds());
|
|
2398
|
+
const sharingReview = traceSync("sharingReview", () => store.sharingReviewSummary(project));
|
|
2399
|
+
const coordinatorSnapshot = await coordinatorStatusSnapshot(store, config);
|
|
2400
|
+
const coordinator = traceSync("coordinator", () => redactCoordinatorStatus(coordinatorSnapshot, showDiag));
|
|
2319
2401
|
let joinRequests = [];
|
|
2320
2402
|
if (includeJoinRequests && showDiag && config.syncCoordinatorAdminSecret) try {
|
|
2321
2403
|
joinRequests = await listCoordinatorJoinRequests(config);
|