@codemem/server 0.33.0 → 0.34.0

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, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, MemoryStore, SYNC_CAPABILITY_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, canonicalWorkspaceIdentity, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, createCoordinatorReciprocalApproval, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isScopedSyncCapability, isSummaryLikeMemory, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeAddress, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recordNonce, rejectInboundScopeFailures, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, summarizeInboundScopeRejections, syncScopeResetRequiredPayload, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, verifySignature, writeCodememConfigFile } from "@codemem/core";
3
+ import { ACCESS_CLEANUP_OP_TYPE, CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, MemoryStore, SYNC_CAPABILITY_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, canonicalWorkspaceIdentity, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, createCoordinatorReciprocalApproval, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, diagnoseStalePeerReceivedRows, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isScopedSyncCapability, isSummaryLikeMemory, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeAddress, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recordNonce, rejectInboundScopeFailures, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, summarizeInboundScopeRejections, syncScopeResetRequiredPayload, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, 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";
@@ -1528,6 +1528,57 @@ function totalUsageEvents(rows) {
1528
1528
  });
1529
1529
  }
1530
1530
  /**
1531
+ * Short-lived cache for the computed /api/usage payload.
1532
+ *
1533
+ * The health dot polls /api/usage on every 5s refresh regardless of the
1534
+ * active tab, and the computation scans the full usage_events table to apply
1535
+ * scope visibility before aggregating. The resulting token totals are
1536
+ * cumulative dashboard figures, so a few seconds of staleness is acceptable
1537
+ * in exchange for collapsing the repeated full-table scan on the polling
1538
+ * loop. Keyed by db path + scope identity + project filter so distinct
1539
+ * stores/projects never share an entry.
1540
+ */
1541
+ var usagePayloadCache = /* @__PURE__ */ new Map();
1542
+ var USAGE_PAYLOAD_CACHE_MS = 1e4;
1543
+ /**
1544
+ * Cheap fingerprint of the state that drives scope visibility
1545
+ * (`scope_memberships` / `replication_scopes`). Folding this into the usage
1546
+ * cache key ensures a membership/scope status change — e.g. a revocation —
1547
+ * invalidates any cached payload immediately instead of letting a device keep
1548
+ * reading a scope it can no longer see for up to the TTL. These tables are
1549
+ * small, so the aggregate is negligible next to the usage_events scan the
1550
+ * cache exists to avoid. Fails safe: any error forces a unique value so the
1551
+ * request bypasses the cache and recomputes visibility from scratch.
1552
+ */
1553
+ function scopeVisibilityGeneration(store) {
1554
+ try {
1555
+ const row = store.db.prepare(`SELECT
1556
+ (SELECT COUNT(*) FROM scope_memberships) AS m_count,
1557
+ (SELECT COUNT(*) FROM scope_memberships WHERE status = 'active') AS m_active,
1558
+ (SELECT COALESCE(MAX(updated_at), '') FROM scope_memberships) AS m_updated,
1559
+ (SELECT COALESCE(MAX(membership_epoch), 0) FROM scope_memberships) AS m_epoch,
1560
+ (SELECT COUNT(*) FROM replication_scopes) AS s_count,
1561
+ (SELECT COUNT(*) FROM replication_scopes WHERE status = 'active') AS s_active,
1562
+ (SELECT COALESCE(MAX(updated_at), '') FROM replication_scopes) AS s_updated`).get();
1563
+ if (!row) return "no-scope-state";
1564
+ return [
1565
+ row.m_count,
1566
+ row.m_active,
1567
+ row.m_updated,
1568
+ row.m_epoch,
1569
+ row.s_count,
1570
+ row.s_active,
1571
+ row.s_updated
1572
+ ].join(":");
1573
+ } catch {
1574
+ return `bypass-${Date.now()}-${Math.random()}`;
1575
+ }
1576
+ }
1577
+ function usageCacheKey(store, projectFilter) {
1578
+ const visibilityGeneration = scopeVisibilityGeneration(store);
1579
+ return `${store.dbPath}|${store.actorId}|${store.deviceId}|${projectFilter ?? ""}|${visibilityGeneration}`;
1580
+ }
1581
+ /**
1531
1582
  * Create stats routes. The store factory is called per-request to get a
1532
1583
  * fresh connection (matching the Python viewer pattern).
1533
1584
  */
@@ -1575,6 +1626,10 @@ function statsRoutes(getStore) {
1575
1626
  const store = getStore();
1576
1627
  {
1577
1628
  const projectFilter = c.req.query("project") || null;
1629
+ const cacheKey = usageCacheKey(store, projectFilter);
1630
+ const nowMs = Date.now();
1631
+ const cached = usagePayloadCache.get(cacheKey);
1632
+ if (cached && nowMs < cached.expiresAtMs) return c.json(cached.payload);
1578
1633
  const loadUsageRows = (project) => {
1579
1634
  return (project ? store.db.prepare(`SELECT usage_events.id, usage_events.session_id, usage_events.event,
1580
1635
  usage_events.tokens_read, usage_events.tokens_written, usage_events.tokens_saved,
@@ -1602,7 +1657,7 @@ function statsRoutes(getStore) {
1602
1657
  ...row,
1603
1658
  metadata_json: sanitizePackUsageMetadata(row.metadata_json, recentVisibility)
1604
1659
  }));
1605
- return c.json({
1660
+ const payload = {
1606
1661
  project: projectFilter,
1607
1662
  events: projectFilter ? eventsFiltered : eventsGlobal,
1608
1663
  totals: projectFilter ? totalsFiltered : totalsGlobal,
@@ -1611,7 +1666,12 @@ function statsRoutes(getStore) {
1611
1666
  events_filtered: eventsFiltered,
1612
1667
  totals_filtered: totalsFiltered,
1613
1668
  recent_packs: recentPacks
1669
+ };
1670
+ usagePayloadCache.set(cacheKey, {
1671
+ payload,
1672
+ expiresAtMs: Date.now() + USAGE_PAYLOAD_CACHE_MS
1614
1673
  });
1674
+ return c.json(payload);
1615
1675
  }
1616
1676
  });
1617
1677
  return app;
@@ -2616,6 +2676,7 @@ function mapSyncAttemptRow(row, showDiag, addresses) {
2616
2676
  };
2617
2677
  }
2618
2678
  var SYNC_SCOPE_REJECTION_WINDOW_SECONDS = 1440 * 60;
2679
+ var CLEANUP_DIAGNOSTICS_MAX_ROWS = 250;
2619
2680
  function recentScopeRejectionsByPeer(store) {
2620
2681
  const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_SCOPE_REJECTION_WINDOW_SECONDS * 1e3)).toISOString();
2621
2682
  const summaries = summarizeInboundScopeRejections(store.db, { sinceIso: cutoff });
@@ -2627,6 +2688,48 @@ function recentScopeRejectionsByPeer(store) {
2627
2688
  }
2628
2689
  return map;
2629
2690
  }
2691
+ function cleanupDiagnostics(store, localDeviceId, showDiag) {
2692
+ const opRows = store.db.prepare(`SELECT
2693
+ SUM(CASE WHEN device_id = ? THEN 1 ELSE 0 END) AS source_authored,
2694
+ SUM(CASE WHEN device_id != ? THEN 1 ELSE 0 END) AS applied,
2695
+ MAX(created_at) AS latest_at
2696
+ FROM replication_ops
2697
+ WHERE op_type = ?`).get(localDeviceId ?? "", localDeviceId ?? "", ACCESS_CLEANUP_OP_TYPE);
2698
+ const stale = localDeviceId && showDiag ? diagnoseStalePeerReceivedRows(store.db, {
2699
+ localDeviceId,
2700
+ maxRows: CLEANUP_DIAGNOSTICS_MAX_ROWS
2701
+ }) : {
2702
+ ambiguous: [],
2703
+ checked: 0,
2704
+ retained: 0,
2705
+ would_delete: 0,
2706
+ would_delete_memory_ids: []
2707
+ };
2708
+ const ambiguousByReason = {};
2709
+ for (const item of stale.ambiguous) ambiguousByReason[item.reason] = (ambiguousByReason[item.reason] ?? 0) + 1;
2710
+ const sourceAuthored = Number(opRows?.source_authored ?? 0);
2711
+ const applied = Number(opRows?.applied ?? 0);
2712
+ const ambiguous = stale.ambiguous.length;
2713
+ return {
2714
+ state: stale.would_delete ? "cleanup_pending" : ambiguous ? "needs_review" : applied ? "cleanup_applied" : sourceAuthored ? "cleanup_announced" : "clear",
2715
+ access_cleanup_ops: {
2716
+ source_authored: sourceAuthored,
2717
+ applied,
2718
+ latest_at: opRows?.latest_at ?? null
2719
+ },
2720
+ stale_peer_rows: {
2721
+ checked: stale.checked,
2722
+ diagnostic_limit: showDiag ? CLEANUP_DIAGNOSTICS_MAX_ROWS : 0,
2723
+ scan_skipped: !showDiag || !localDeviceId,
2724
+ would_remove: stale.would_delete,
2725
+ ambiguous,
2726
+ ambiguous_by_reason: ambiguousByReason,
2727
+ retained: stale.retained,
2728
+ items: showDiag ? stale.ambiguous.slice(0, 25) : void 0
2729
+ },
2730
+ redacted: !showDiag
2731
+ };
2732
+ }
2630
2733
  function legacySharedReviewSummary(store) {
2631
2734
  const rows = legacySharedReviewRows(store);
2632
2735
  const memoryCount = rows.length;
@@ -2862,7 +2965,7 @@ function legacySharedReviewReassignmentPreview(store, input) {
2862
2965
  scope_id: input.scopeId,
2863
2966
  skipped_memory_count: rows.length - reassignableRows.length,
2864
2967
  target_scope_label: targetScope.label || targetScope.scope_id,
2865
- warning: "This changes future sync authorization for reassignable local memories. It does not erase data already copied to peers under legacy-shared-review.",
2968
+ warning: "This device can reassign only locally owned memories. Peer-owned copies must be fixed on their source device. Online compatible peers should converge after syncing, but offline devices, backups, copied databases, malicious peers, or old versions may retain old copies.",
2866
2969
  workspace_identity: input.workspaceIdentity
2867
2970
  };
2868
2971
  }
@@ -3319,6 +3422,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3319
3422
  include: config.syncProjectsInclude,
3320
3423
  exclude: config.syncProjectsExclude
3321
3424
  },
3425
+ cleanup_diagnostics: cleanupDiagnostics(store, deviceRow?.device_id ?? null, showDiag),
3322
3426
  redacted: !showDiag
3323
3427
  };
3324
3428
  if (showDiag) {
@@ -3725,7 +3829,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3725
3829
  guardrail_warnings: analysis.warnings
3726
3830
  }, 409);
3727
3831
  }
3728
- const mapping = upsertProjectScopeSettingsMapping(store.db, { ...mappingInput });
3832
+ const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
3833
+ const mapping = upsertProjectScopeSettingsMapping(store.db, {
3834
+ deviceId,
3835
+ ...mappingInput
3836
+ });
3729
3837
  return c.json({
3730
3838
  ok: true,
3731
3839
  mapping,
@@ -3764,7 +3872,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3764
3872
  guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
3765
3873
  }, 409);
3766
3874
  }
3767
- const saveMappings = store.db.transaction(() => mappingInputs.map((mappingInput) => upsertProjectScopeSettingsMapping(store.db, mappingInput)));
3875
+ const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
3876
+ const saveMappings = store.db.transaction(() => mappingInputs.map((mappingInput) => upsertProjectScopeSettingsMapping(store.db, {
3877
+ deviceId,
3878
+ ...mappingInput
3879
+ })));
3768
3880
  return c.json({
3769
3881
  ok: true,
3770
3882
  mappings: saveMappings(),