@codemem/server 0.33.0-alpha.3 → 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;
@@ -2653,6 +2756,7 @@ function legacySharedReviewSummary(store) {
2653
2756
  if (isOwnedBySelf) existing.reassignable_memory_count += 1;
2654
2757
  else existing.peer_owned_memory_count += 1;
2655
2758
  if (rowUpdatedAt && (!existing.last_updated_at || rowUpdatedAt > existing.last_updated_at)) existing.last_updated_at = rowUpdatedAt;
2759
+ if (existing.memory_samples.length < LEGACY_SHARED_REVIEW_SAMPLE_LIMIT) existing.memory_samples.push(legacySharedReviewMemorySample(row, isOwnedBySelf));
2656
2760
  continue;
2657
2761
  }
2658
2762
  groupsByIdentity.set(identity.value, {
@@ -2663,6 +2767,7 @@ function legacySharedReviewSummary(store) {
2663
2767
  reassignable_memory_count: isOwnedBySelf ? 1 : 0,
2664
2768
  peer_owned_memory_count: isOwnedBySelf ? 0 : 1,
2665
2769
  last_updated_at: rowUpdatedAt,
2770
+ memory_samples: [legacySharedReviewMemorySample(row, isOwnedBySelf)],
2666
2771
  suggested_scope_id: suggestedScope ?? null,
2667
2772
  suggestion_reason: suggestedScope ? candidate?.suggestion_reason ?? "Existing project mapping can be reviewed as a destination, but legacy data is not promoted automatically." : null
2668
2773
  });
@@ -2684,13 +2789,38 @@ function legacySharedReviewSummary(store) {
2684
2789
  target_scopes: targetScopes
2685
2790
  };
2686
2791
  }
2792
+ var LEGACY_SHARED_REVIEW_SAMPLE_LIMIT = 3;
2793
+ var LEGACY_SHARED_REVIEW_BODY_PREVIEW_SQL_LIMIT = 360;
2794
+ function previewText(value, limit = 180) {
2795
+ const normalized = value?.replace(/\s+/g, " ").trim();
2796
+ if (!normalized) return null;
2797
+ return normalized.length > limit ? `${normalized.slice(0, limit - 1)}…` : normalized;
2798
+ }
2799
+ function legacySharedReviewMemorySample(row, isOwnedBySelf) {
2800
+ return {
2801
+ id: row.id,
2802
+ kind: row.kind,
2803
+ title: previewText(row.title, 120) ?? `Memory ${row.id}`,
2804
+ body_preview: previewText(row.body_text),
2805
+ created_at: row.created_at ?? null,
2806
+ updated_at: row.updated_at ?? null,
2807
+ ownership: isOwnedBySelf ? "local" : "peer",
2808
+ project: row.project ?? null,
2809
+ cwd: row.cwd ?? null,
2810
+ git_remote: row.git_remote ?? null
2811
+ };
2812
+ }
2687
2813
  function legacySharedReviewRows(store) {
2688
2814
  return store.db.prepare(`SELECT m.id,
2689
2815
  m.rev,
2690
2816
  m.active,
2691
2817
  m.deleted_at,
2818
+ m.created_at,
2692
2819
  m.updated_at,
2693
2820
  m.scope_id,
2821
+ m.kind,
2822
+ m.title,
2823
+ substr(m.body_text, 1, ?) AS body_text,
2694
2824
  s.project,
2695
2825
  s.cwd,
2696
2826
  s.git_remote,
@@ -2703,7 +2833,8 @@ function legacySharedReviewRows(store) {
2703
2833
  LEFT JOIN sessions s ON s.id = m.session_id
2704
2834
  WHERE m.scope_id = ?
2705
2835
  AND m.active = 1
2706
- AND m.deleted_at IS NULL`).all(LEGACY_SHARED_REVIEW_SCOPE_ID);
2836
+ AND m.deleted_at IS NULL
2837
+ ORDER BY COALESCE(m.updated_at, m.created_at, '') DESC, m.id DESC`).all(LEGACY_SHARED_REVIEW_BODY_PREVIEW_SQL_LIMIT, LEGACY_SHARED_REVIEW_SCOPE_ID);
2707
2838
  }
2708
2839
  function legacySharedReviewConfirmationToken(input) {
2709
2840
  const ownedBySelf = input.store.buildOwnershipPredicate();
@@ -2834,7 +2965,7 @@ function legacySharedReviewReassignmentPreview(store, input) {
2834
2965
  scope_id: input.scopeId,
2835
2966
  skipped_memory_count: rows.length - reassignableRows.length,
2836
2967
  target_scope_label: targetScope.label || targetScope.scope_id,
2837
- 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.",
2838
2969
  workspace_identity: input.workspaceIdentity
2839
2970
  };
2840
2971
  }
@@ -3291,6 +3422,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3291
3422
  include: config.syncProjectsInclude,
3292
3423
  exclude: config.syncProjectsExclude
3293
3424
  },
3425
+ cleanup_diagnostics: cleanupDiagnostics(store, deviceRow?.device_id ?? null, showDiag),
3294
3426
  redacted: !showDiag
3295
3427
  };
3296
3428
  if (showDiag) {
@@ -3697,7 +3829,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3697
3829
  guardrail_warnings: analysis.warnings
3698
3830
  }, 409);
3699
3831
  }
3700
- 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
+ });
3701
3837
  return c.json({
3702
3838
  ok: true,
3703
3839
  mapping,
@@ -3736,7 +3872,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3736
3872
  guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
3737
3873
  }, 409);
3738
3874
  }
3739
- 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
+ })));
3740
3880
  return c.json({
3741
3881
  ok: true,
3742
3882
  mappings: saveMappings(),