@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 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
- return store.db.prepare(`SELECT memory_items.* FROM ${from}
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).map((row) => ({
622
- ...row,
623
- kind: canonicalMemoryKind(row.kind ?? null, row.metadata_json),
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 coordinator = redactCoordinatorStatus(await coordinatorStatusSnapshot(store, config), showDiag);
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);