@codemem/server 0.30.0-alpha.3 → 0.30.0-alpha.4

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, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, MemoryStore, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, 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, deleteProjectScopeSettingsMapping, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listProjectScopeCandidates, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, rejectInboundScopeFailures, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, summarizeInboundScopeRejections, syncScopeResetRequiredPayload, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, verifySignature, writeCodememConfigFile } from "@codemem/core";
3
+ import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, MemoryStore, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, 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, deleteProjectScopeSettingsMapping, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listProjectScopeCandidates, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, 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";
@@ -1413,31 +1413,80 @@ function toUsageRow(row, metadata) {
1413
1413
  metadata_json: metadata
1414
1414
  };
1415
1415
  }
1416
- function visibleMemoryIds(store, value) {
1416
+ function metadataMemoryIds(value) {
1417
1417
  if (!Array.isArray(value)) return [];
1418
- return value.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item) && store.get(item) != null);
1418
+ return value.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item) && item > 0);
1419
1419
  }
1420
- function sessionHasVisibleMemory(store, sessionId) {
1421
- const filterResult = buildFilterClausesWithContext({ session_id: sessionId }, {
1420
+ function chunkNumbers(values, chunkSize = 500) {
1421
+ const chunks = [];
1422
+ for (let start = 0; start < values.length; start += chunkSize) chunks.push(values.slice(start, start + chunkSize));
1423
+ return chunks;
1424
+ }
1425
+ function visibleMemoryIdSet(store, memoryIds) {
1426
+ if (memoryIds.size === 0) return /* @__PURE__ */ new Set();
1427
+ const filterResult = buildFilterClausesWithContext(null, {
1422
1428
  actorId: store.actorId,
1423
1429
  deviceId: store.deviceId,
1424
1430
  enforceScopeVisibility: true
1425
1431
  });
1426
- const clauses = ["memory_items.active = 1", ...filterResult.clauses];
1427
- return store.db.prepare(`SELECT 1 AS found FROM memory_items WHERE ${clauses.join(" AND ")} LIMIT 1`).get(...filterResult.params) != null;
1432
+ const visibleIds = /* @__PURE__ */ new Set();
1433
+ for (const chunk of chunkNumbers([...memoryIds])) {
1434
+ const clauses = [
1435
+ "memory_items.active = 1",
1436
+ `memory_items.id IN (${chunk.map(() => "?").join(", ")})`,
1437
+ ...filterResult.clauses
1438
+ ];
1439
+ const rows = store.db.prepare(`SELECT memory_items.id FROM memory_items WHERE ${clauses.join(" AND ")}`).all(...chunk, ...filterResult.params);
1440
+ for (const row of rows) visibleIds.add(Number(row.id));
1441
+ }
1442
+ return visibleIds;
1428
1443
  }
1429
- function usageRowVisible(store, row) {
1444
+ function visibleSessionIdSet(store, sessionIds) {
1445
+ if (sessionIds.size === 0) return /* @__PURE__ */ new Set();
1446
+ const filterResult = buildFilterClausesWithContext(null, {
1447
+ actorId: store.actorId,
1448
+ deviceId: store.deviceId,
1449
+ enforceScopeVisibility: true
1450
+ });
1451
+ const visibleIds = /* @__PURE__ */ new Set();
1452
+ for (const chunk of chunkNumbers([...sessionIds])) {
1453
+ const clauses = [
1454
+ "memory_items.active = 1",
1455
+ `memory_items.session_id IN (${chunk.map(() => "?").join(", ")})`,
1456
+ ...filterResult.clauses
1457
+ ];
1458
+ const rows = store.db.prepare(`SELECT DISTINCT memory_items.session_id AS session_id
1459
+ FROM memory_items
1460
+ WHERE ${clauses.join(" AND ")}`).all(...chunk, ...filterResult.params);
1461
+ for (const row of rows) if (typeof row.session_id === "number") visibleIds.add(row.session_id);
1462
+ }
1463
+ return visibleIds;
1464
+ }
1465
+ function buildUsageVisibility(store, rows) {
1466
+ const memoryIds = /* @__PURE__ */ new Set();
1467
+ const sessionIds = /* @__PURE__ */ new Set();
1468
+ for (const row of rows) {
1469
+ if (typeof row.session_id === "number") sessionIds.add(row.session_id);
1470
+ for (const key of MEMORY_ID_METADATA_KEYS) for (const memoryId of metadataMemoryIds(row.metadata_json?.[key])) memoryIds.add(memoryId);
1471
+ }
1472
+ return {
1473
+ visibleMemoryIds: visibleMemoryIdSet(store, memoryIds),
1474
+ visibleSessionIds: visibleSessionIdSet(store, sessionIds)
1475
+ };
1476
+ }
1477
+ function usageRowVisible(row, visibility) {
1478
+ const rowSessionVisible = row.session_id == null || visibility.visibleSessionIds.has(row.session_id);
1430
1479
  const packItemIds = row.metadata_json?.pack_item_ids;
1431
1480
  if (Array.isArray(packItemIds)) {
1432
- if (packItemIds.length === 0) return row.session_id != null && sessionHasVisibleMemory(store, row.session_id);
1433
- return visibleMemoryIds(store, packItemIds).length > 0;
1481
+ if (packItemIds.length === 0) return row.session_id != null && visibility.visibleSessionIds.has(row.session_id);
1482
+ return rowSessionVisible && metadataMemoryIds(packItemIds).some((memoryId) => visibility.visibleMemoryIds.has(memoryId));
1434
1483
  }
1435
- return row.session_id != null && sessionHasVisibleMemory(store, row.session_id);
1484
+ return row.session_id != null && visibility.visibleSessionIds.has(row.session_id);
1436
1485
  }
1437
- function sanitizePackUsageMetadata(store, metadata) {
1486
+ function sanitizePackUsageMetadata(metadata, visibility) {
1438
1487
  if (!metadata) return null;
1439
1488
  const sanitized = { ...metadata };
1440
- for (const key of MEMORY_ID_METADATA_KEYS) if (Array.isArray(sanitized[key])) sanitized[key] = visibleMemoryIds(store, sanitized[key]);
1489
+ for (const key of MEMORY_ID_METADATA_KEYS) if (Array.isArray(sanitized[key])) sanitized[key] = metadataMemoryIds(sanitized[key]).filter((memoryId) => visibility.visibleMemoryIds.has(memoryId));
1441
1490
  return sanitized;
1442
1491
  }
1443
1492
  function summarizeUsageEvents(rows) {
@@ -1529,17 +1578,22 @@ function statsRoutes(getStore) {
1529
1578
  ORDER BY usage_events.created_at DESC`).all(project) : store.db.prepare(`SELECT id, session_id, event, tokens_read, tokens_written, tokens_saved,
1530
1579
  created_at, metadata_json
1531
1580
  FROM usage_events
1532
- ORDER BY created_at DESC`).all()).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json))).filter((row) => usageRowVisible(store, row));
1581
+ ORDER BY created_at DESC`).all()).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json)));
1533
1582
  };
1534
- const globalRows = loadUsageRows();
1535
- const filteredRows = projectFilter ? loadUsageRows(projectFilter) : null;
1583
+ const loadedGlobalRows = loadUsageRows();
1584
+ const globalVisibility = buildUsageVisibility(store, loadedGlobalRows);
1585
+ const globalRows = loadedGlobalRows.filter((row) => usageRowVisible(row, globalVisibility));
1586
+ const loadedFilteredRows = projectFilter ? loadUsageRows(projectFilter) : null;
1587
+ const filteredVisibility = loadedFilteredRows ? buildUsageVisibility(store, loadedFilteredRows) : null;
1588
+ const filteredRows = loadedFilteredRows ? loadedFilteredRows.filter((row) => usageRowVisible(row, filteredVisibility ?? globalVisibility)) : null;
1536
1589
  const eventsGlobal = summarizeUsageEvents(globalRows);
1537
1590
  const totalsGlobal = totalUsageEvents(globalRows);
1538
1591
  const eventsFiltered = filteredRows ? summarizeUsageEvents(filteredRows) : null;
1539
1592
  const totalsFiltered = filteredRows ? totalUsageEvents(filteredRows) : null;
1593
+ const recentVisibility = filteredVisibility ?? globalVisibility;
1540
1594
  const recentPacks = (filteredRows ?? globalRows).filter((row) => row.event === "pack").slice(0, 10).map((row) => ({
1541
1595
  ...row,
1542
- metadata_json: sanitizePackUsageMetadata(store, row.metadata_json)
1596
+ metadata_json: sanitizePackUsageMetadata(row.metadata_json, recentVisibility)
1543
1597
  }));
1544
1598
  return c.json({
1545
1599
  project: projectFilter,
@@ -1817,6 +1871,43 @@ function unauthorizedPayload(reason, exposeReason = false) {
1817
1871
  };
1818
1872
  return { error: "unauthorized" };
1819
1873
  }
1874
+ var SYNC_AUTH_STORE_BUSY_REASON = "sync_auth_store_busy";
1875
+ function isSqliteBusy(err) {
1876
+ if (!err || typeof err !== "object") return false;
1877
+ const code = "code" in err ? String(err.code ?? "") : "";
1878
+ const message = err instanceof Error ? err.message : "";
1879
+ return code === "SQLITE_BUSY" || message.includes("database is locked");
1880
+ }
1881
+ function recordSyncAuthNonce(store, deviceId, nonce, createdAt) {
1882
+ try {
1883
+ if (!recordNonce(store.db, deviceId, nonce, createdAt)) return {
1884
+ ok: false,
1885
+ reason: "nonce_replay",
1886
+ deviceId
1887
+ };
1888
+ } catch (err) {
1889
+ if (isSqliteBusy(err)) return {
1890
+ ok: false,
1891
+ reason: SYNC_AUTH_STORE_BUSY_REASON,
1892
+ deviceId
1893
+ };
1894
+ throw err;
1895
+ }
1896
+ const cutoff = (/* @__PURE__ */ new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1e3)).toISOString();
1897
+ try {
1898
+ cleanupNonces(store.db, cutoff);
1899
+ } catch (err) {
1900
+ if (!isSqliteBusy(err)) throw err;
1901
+ }
1902
+ return null;
1903
+ }
1904
+ function isSyncAuthStoreBusy(auth) {
1905
+ return !auth.ok && auth.reason === SYNC_AUTH_STORE_BUSY_REASON;
1906
+ }
1907
+ function syncAuthStoreBusyResponse(c) {
1908
+ c.header("Retry-After", "1");
1909
+ return c.json({ error: SYNC_AUTH_STORE_BUSY_REASON }, 503);
1910
+ }
1820
1911
  function authorizeSyncRequest(store, request, body) {
1821
1912
  const deviceId = (request.header("X-Opencode-Device") ?? "").trim();
1822
1913
  const signature = request.header("X-Opencode-Signature") ?? "";
@@ -1869,14 +1960,8 @@ function authorizeSyncRequest(store, request, body) {
1869
1960
  reason: "invalid_signature",
1870
1961
  deviceId
1871
1962
  };
1872
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1873
- if (!recordNonce(store.db, deviceId, nonce, createdAt)) return {
1874
- ok: false,
1875
- reason: "nonce_replay",
1876
- deviceId
1877
- };
1878
- const cutoff = (/* @__PURE__ */ new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1e3)).toISOString();
1879
- cleanupNonces(store.db, cutoff);
1963
+ const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
1964
+ if (nonceResult) return nonceResult;
1880
1965
  return {
1881
1966
  ok: true,
1882
1967
  reason: "ok",
@@ -1986,14 +2071,8 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
1986
2071
  reason: "bootstrap_grant_invalid_signature",
1987
2072
  deviceId
1988
2073
  };
1989
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1990
- if (!recordNonce(store.db, deviceId, nonce, createdAt)) return {
1991
- ok: false,
1992
- reason: "nonce_replay",
1993
- deviceId
1994
- };
1995
- const cutoff = (/* @__PURE__ */ new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1e3)).toISOString();
1996
- cleanupNonces(store.db, cutoff);
2074
+ const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
2075
+ if (nonceResult) return nonceResult;
1997
2076
  return {
1998
2077
  ok: true,
1999
2078
  reason: "ok",
@@ -2367,6 +2446,21 @@ function recentScopeRejectionsByPeer(store) {
2367
2446
  }
2368
2447
  return map;
2369
2448
  }
2449
+ function legacySharedReviewSummary(store) {
2450
+ const row = store.db.prepare(`SELECT COUNT(*) AS memory_count,
2451
+ MAX(updated_at) AS last_updated_at
2452
+ FROM memory_items
2453
+ WHERE scope_id = ?
2454
+ AND active = 1
2455
+ AND deleted_at IS NULL`).get(LEGACY_SHARED_REVIEW_SCOPE_ID);
2456
+ const memoryCount = Number(row?.memory_count ?? 0);
2457
+ return {
2458
+ scope_id: LEGACY_SHARED_REVIEW_SCOPE_ID,
2459
+ memory_count: memoryCount,
2460
+ has_data: memoryCount > 0,
2461
+ last_updated_at: row?.last_updated_at ?? null
2462
+ };
2463
+ }
2370
2464
  var SYNC_DIRECTION_WINDOW_SECONDS = 1440 * 60;
2371
2465
  function recentPeerOps(store) {
2372
2466
  const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_DIRECTION_WINDOW_SECONDS * 1e3)).toISOString();
@@ -2438,7 +2532,8 @@ var PEERS_QUERY = `
2438
2532
  SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
2439
2533
  p.last_seen_at, p.last_sync_at, p.last_error,
2440
2534
  p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,
2441
- p.actor_id, a.display_name AS actor_display_name
2535
+ p.actor_id, p.discovered_via_coordinator_id, p.discovered_via_group_id,
2536
+ a.display_name AS actor_display_name
2442
2537
  FROM sync_peers AS p
2443
2538
  LEFT JOIN actors AS a ON a.actor_id = p.actor_id
2444
2539
  ORDER BY name, peer_device_id
@@ -2468,6 +2563,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
2468
2563
  const store = getStore();
2469
2564
  return (async () => {
2470
2565
  let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
2566
+ if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
2471
2567
  let preauthChecked = false;
2472
2568
  let bootstrapAttempted = false;
2473
2569
  if (!auth.ok) {
@@ -2483,6 +2579,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
2483
2579
  auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
2484
2580
  bootstrapAttempted = true;
2485
2581
  }
2582
+ if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
2486
2583
  if (!auth.ok) {
2487
2584
  if (bootstrapAttempted) console.warn(`[sync] bootstrap grant auth failed: reason=${auth.reason} grant=${(c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim()} path=${c.req.path}`);
2488
2585
  const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
@@ -2508,6 +2605,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
2508
2605
  app.get("/v1/ops", (c) => {
2509
2606
  const store = getStore();
2510
2607
  const auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
2608
+ if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
2511
2609
  if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason), 401);
2512
2610
  const limited = rateLimitedResponse(c, auth.deviceId, true);
2513
2611
  if (limited) return limited;
@@ -2560,6 +2658,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
2560
2658
  const store = getStore();
2561
2659
  return (async () => {
2562
2660
  let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
2661
+ if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
2563
2662
  let preauthChecked = false;
2564
2663
  let bootstrapAttempted = false;
2565
2664
  if (!auth.ok) {
@@ -2575,6 +2674,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
2575
2674
  auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
2576
2675
  bootstrapAttempted = true;
2577
2676
  }
2677
+ if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
2578
2678
  if (!auth.ok) {
2579
2679
  if (bootstrapAttempted) console.warn(`[sync] bootstrap grant auth failed: reason=${auth.reason} grant=${(c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim()} path=${c.req.path}`);
2580
2680
  const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
@@ -2626,6 +2726,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
2626
2726
  const raw = await readBoundedRequestBytes(c.req.raw, MAX_SYNC_BODY_BYTES);
2627
2727
  if (raw == null) return c.json({ error: "payload_too_large" }, 413);
2628
2728
  const auth = authorizeSyncRequest(store, c.req, raw);
2729
+ if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
2629
2730
  if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason), 401);
2630
2731
  const limited = rateLimitedResponse(c, auth.deviceId, true);
2631
2732
  if (limited) return limited;
@@ -2817,6 +2918,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2817
2918
  ping: {}
2818
2919
  };
2819
2920
  const legacyDevices = traceSync("legacyDevices", () => store.claimableLegacyDeviceIds());
2921
+ const legacyReview = traceSync("legacySharedReview", () => legacySharedReviewSummary(store));
2820
2922
  const sharingReview = traceSync("sharingReview", () => store.sharingReviewSummary(project));
2821
2923
  let joinRequests = [];
2822
2924
  if (includeJoinRequests && showDiag && config.syncCoordinatorAdminSecret) try {
@@ -2851,6 +2953,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2851
2953
  peers: peersItems,
2852
2954
  attempts: attemptsItems.slice(0, 5),
2853
2955
  legacy_devices: legacyDevices,
2956
+ legacy_shared_review: legacyReview,
2854
2957
  sharing_review: sharingReview,
2855
2958
  coordinator
2856
2959
  };