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

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";
@@ -655,6 +655,29 @@ function countVisibleMemoryRows(store, filters) {
655
655
  const row = store.db.prepare(`SELECT COUNT(*) AS total FROM ${from} WHERE ${clauses.join(" AND ")}`).get(...filterResult.params);
656
656
  return Number(row?.total ?? 0);
657
657
  }
658
+ function summaryLikeSqlPredicate() {
659
+ return `(
660
+ LOWER(TRIM(COALESCE(memory_items.kind, ''))) = 'session_summary'
661
+ OR (
662
+ json_valid(COALESCE(memory_items.metadata_json, ''))
663
+ AND (
664
+ COALESCE(json_extract(memory_items.metadata_json, '$.is_summary') = 1, 0)
665
+ OR LOWER(TRIM(COALESCE(json_extract(memory_items.metadata_json, '$.source'), ''))) = 'observer_summary'
666
+ )
667
+ )
668
+ )`;
669
+ }
670
+ function countVisibleObservationRows(store, filters) {
671
+ const filterResult = buildViewerMemoryFilters(store, filters);
672
+ const clauses = [
673
+ "memory_items.active = 1",
674
+ `NOT ${summaryLikeSqlPredicate()}`,
675
+ ...filterResult.clauses
676
+ ];
677
+ const from = filterResult.joinSessions ? "memory_items JOIN sessions ON sessions.id = memory_items.session_id" : "memory_items";
678
+ const row = store.db.prepare(`SELECT COUNT(*) AS total FROM ${from} WHERE ${clauses.join(" AND ")}`).get(...filterResult.params);
679
+ return Number(row?.total ?? 0);
680
+ }
658
681
  function sessionAllowsArtifactAccess(store, sessionId) {
659
682
  const visibleCount = countVisibleMemoryRows(store, { session_id: sessionId });
660
683
  if (visibleCount === 0) return false;
@@ -838,32 +861,16 @@ function memoryRoutes(getStore) {
838
861
  let artifacts;
839
862
  let memories;
840
863
  let observations;
841
- const countObservations = (scopeProject) => {
842
- let offset = 0;
843
- let total = 0;
844
- while (true) {
845
- const page = queryMemoryPage(store, {
846
- limit: 200,
847
- offset,
848
- project: scopeProject
849
- });
850
- if (page.length === 0) break;
851
- total += page.filter((item) => !isSummaryLikeMemory$1(item)).length;
852
- if (page.length < 200) break;
853
- offset += page.length;
854
- }
855
- return total;
856
- };
857
864
  if (project) {
858
865
  prompts = countVisiblePromptRows(store, project);
859
866
  artifacts = countVisibleArtifactRows(store, project);
860
867
  memories = countVisibleMemoryRows(store, { project });
861
- observations = countObservations(project);
868
+ observations = countVisibleObservationRows(store, { project });
862
869
  } else {
863
870
  prompts = countVisiblePromptRows(store);
864
871
  artifacts = countVisibleArtifactRows(store);
865
872
  memories = countVisibleMemoryRows(store);
866
- observations = countObservations();
873
+ observations = countVisibleObservationRows(store);
867
874
  }
868
875
  const total = prompts + artifacts + memories;
869
876
  return c.json({
@@ -1413,31 +1420,80 @@ function toUsageRow(row, metadata) {
1413
1420
  metadata_json: metadata
1414
1421
  };
1415
1422
  }
1416
- function visibleMemoryIds(store, value) {
1423
+ function metadataMemoryIds(value) {
1417
1424
  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);
1425
+ return value.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item) && item > 0);
1426
+ }
1427
+ function chunkNumbers(values, chunkSize = 500) {
1428
+ const chunks = [];
1429
+ for (let start = 0; start < values.length; start += chunkSize) chunks.push(values.slice(start, start + chunkSize));
1430
+ return chunks;
1419
1431
  }
1420
- function sessionHasVisibleMemory(store, sessionId) {
1421
- const filterResult = buildFilterClausesWithContext({ session_id: sessionId }, {
1432
+ function visibleMemoryIdSet(store, memoryIds) {
1433
+ if (memoryIds.size === 0) return /* @__PURE__ */ new Set();
1434
+ const filterResult = buildFilterClausesWithContext(null, {
1422
1435
  actorId: store.actorId,
1423
1436
  deviceId: store.deviceId,
1424
1437
  enforceScopeVisibility: true
1425
1438
  });
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;
1439
+ const visibleIds = /* @__PURE__ */ new Set();
1440
+ for (const chunk of chunkNumbers([...memoryIds])) {
1441
+ const clauses = [
1442
+ "memory_items.active = 1",
1443
+ `memory_items.id IN (${chunk.map(() => "?").join(", ")})`,
1444
+ ...filterResult.clauses
1445
+ ];
1446
+ const rows = store.db.prepare(`SELECT memory_items.id FROM memory_items WHERE ${clauses.join(" AND ")}`).all(...chunk, ...filterResult.params);
1447
+ for (const row of rows) visibleIds.add(Number(row.id));
1448
+ }
1449
+ return visibleIds;
1428
1450
  }
1429
- function usageRowVisible(store, row) {
1451
+ function visibleSessionIdSet(store, sessionIds) {
1452
+ if (sessionIds.size === 0) return /* @__PURE__ */ new Set();
1453
+ const filterResult = buildFilterClausesWithContext(null, {
1454
+ actorId: store.actorId,
1455
+ deviceId: store.deviceId,
1456
+ enforceScopeVisibility: true
1457
+ });
1458
+ const visibleIds = /* @__PURE__ */ new Set();
1459
+ for (const chunk of chunkNumbers([...sessionIds])) {
1460
+ const clauses = [
1461
+ "memory_items.active = 1",
1462
+ `memory_items.session_id IN (${chunk.map(() => "?").join(", ")})`,
1463
+ ...filterResult.clauses
1464
+ ];
1465
+ const rows = store.db.prepare(`SELECT DISTINCT memory_items.session_id AS session_id
1466
+ FROM memory_items
1467
+ WHERE ${clauses.join(" AND ")}`).all(...chunk, ...filterResult.params);
1468
+ for (const row of rows) if (typeof row.session_id === "number") visibleIds.add(row.session_id);
1469
+ }
1470
+ return visibleIds;
1471
+ }
1472
+ function buildUsageVisibility(store, rows) {
1473
+ const memoryIds = /* @__PURE__ */ new Set();
1474
+ const sessionIds = /* @__PURE__ */ new Set();
1475
+ for (const row of rows) {
1476
+ if (typeof row.session_id === "number") sessionIds.add(row.session_id);
1477
+ for (const key of MEMORY_ID_METADATA_KEYS) for (const memoryId of metadataMemoryIds(row.metadata_json?.[key])) memoryIds.add(memoryId);
1478
+ }
1479
+ return {
1480
+ visibleMemoryIds: visibleMemoryIdSet(store, memoryIds),
1481
+ visibleSessionIds: visibleSessionIdSet(store, sessionIds)
1482
+ };
1483
+ }
1484
+ function usageRowVisible(row, visibility) {
1485
+ const rowSessionVisible = row.session_id == null || visibility.visibleSessionIds.has(row.session_id);
1430
1486
  const packItemIds = row.metadata_json?.pack_item_ids;
1431
1487
  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;
1488
+ if (packItemIds.length === 0) return row.session_id != null && visibility.visibleSessionIds.has(row.session_id);
1489
+ return rowSessionVisible && metadataMemoryIds(packItemIds).some((memoryId) => visibility.visibleMemoryIds.has(memoryId));
1434
1490
  }
1435
- return row.session_id != null && sessionHasVisibleMemory(store, row.session_id);
1491
+ return row.session_id != null && visibility.visibleSessionIds.has(row.session_id);
1436
1492
  }
1437
- function sanitizePackUsageMetadata(store, metadata) {
1493
+ function sanitizePackUsageMetadata(metadata, visibility) {
1438
1494
  if (!metadata) return null;
1439
1495
  const sanitized = { ...metadata };
1440
- for (const key of MEMORY_ID_METADATA_KEYS) if (Array.isArray(sanitized[key])) sanitized[key] = visibleMemoryIds(store, sanitized[key]);
1496
+ 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
1497
  return sanitized;
1442
1498
  }
1443
1499
  function summarizeUsageEvents(rows) {
@@ -1529,17 +1585,22 @@ function statsRoutes(getStore) {
1529
1585
  ORDER BY usage_events.created_at DESC`).all(project) : store.db.prepare(`SELECT id, session_id, event, tokens_read, tokens_written, tokens_saved,
1530
1586
  created_at, metadata_json
1531
1587
  FROM usage_events
1532
- ORDER BY created_at DESC`).all()).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json))).filter((row) => usageRowVisible(store, row));
1588
+ ORDER BY created_at DESC`).all()).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json)));
1533
1589
  };
1534
- const globalRows = loadUsageRows();
1535
- const filteredRows = projectFilter ? loadUsageRows(projectFilter) : null;
1590
+ const loadedGlobalRows = loadUsageRows();
1591
+ const globalVisibility = buildUsageVisibility(store, loadedGlobalRows);
1592
+ const globalRows = loadedGlobalRows.filter((row) => usageRowVisible(row, globalVisibility));
1593
+ const loadedFilteredRows = projectFilter ? loadUsageRows(projectFilter) : null;
1594
+ const filteredVisibility = loadedFilteredRows ? buildUsageVisibility(store, loadedFilteredRows) : null;
1595
+ const filteredRows = loadedFilteredRows ? loadedFilteredRows.filter((row) => usageRowVisible(row, filteredVisibility ?? globalVisibility)) : null;
1536
1596
  const eventsGlobal = summarizeUsageEvents(globalRows);
1537
1597
  const totalsGlobal = totalUsageEvents(globalRows);
1538
1598
  const eventsFiltered = filteredRows ? summarizeUsageEvents(filteredRows) : null;
1539
1599
  const totalsFiltered = filteredRows ? totalUsageEvents(filteredRows) : null;
1600
+ const recentVisibility = filteredVisibility ?? globalVisibility;
1540
1601
  const recentPacks = (filteredRows ?? globalRows).filter((row) => row.event === "pack").slice(0, 10).map((row) => ({
1541
1602
  ...row,
1542
- metadata_json: sanitizePackUsageMetadata(store, row.metadata_json)
1603
+ metadata_json: sanitizePackUsageMetadata(row.metadata_json, recentVisibility)
1543
1604
  }));
1544
1605
  return c.json({
1545
1606
  project: projectFilter,
@@ -1817,6 +1878,43 @@ function unauthorizedPayload(reason, exposeReason = false) {
1817
1878
  };
1818
1879
  return { error: "unauthorized" };
1819
1880
  }
1881
+ var SYNC_AUTH_STORE_BUSY_REASON = "sync_auth_store_busy";
1882
+ function isSqliteBusy(err) {
1883
+ if (!err || typeof err !== "object") return false;
1884
+ const code = "code" in err ? String(err.code ?? "") : "";
1885
+ const message = err instanceof Error ? err.message : "";
1886
+ return code === "SQLITE_BUSY" || message.includes("database is locked");
1887
+ }
1888
+ function recordSyncAuthNonce(store, deviceId, nonce, createdAt) {
1889
+ try {
1890
+ if (!recordNonce(store.db, deviceId, nonce, createdAt)) return {
1891
+ ok: false,
1892
+ reason: "nonce_replay",
1893
+ deviceId
1894
+ };
1895
+ } catch (err) {
1896
+ if (isSqliteBusy(err)) return {
1897
+ ok: false,
1898
+ reason: SYNC_AUTH_STORE_BUSY_REASON,
1899
+ deviceId
1900
+ };
1901
+ throw err;
1902
+ }
1903
+ const cutoff = (/* @__PURE__ */ new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1e3)).toISOString();
1904
+ try {
1905
+ cleanupNonces(store.db, cutoff);
1906
+ } catch (err) {
1907
+ if (!isSqliteBusy(err)) throw err;
1908
+ }
1909
+ return null;
1910
+ }
1911
+ function isSyncAuthStoreBusy(auth) {
1912
+ return !auth.ok && auth.reason === SYNC_AUTH_STORE_BUSY_REASON;
1913
+ }
1914
+ function syncAuthStoreBusyResponse(c) {
1915
+ c.header("Retry-After", "1");
1916
+ return c.json({ error: SYNC_AUTH_STORE_BUSY_REASON }, 503);
1917
+ }
1820
1918
  function authorizeSyncRequest(store, request, body) {
1821
1919
  const deviceId = (request.header("X-Opencode-Device") ?? "").trim();
1822
1920
  const signature = request.header("X-Opencode-Signature") ?? "";
@@ -1869,14 +1967,8 @@ function authorizeSyncRequest(store, request, body) {
1869
1967
  reason: "invalid_signature",
1870
1968
  deviceId
1871
1969
  };
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);
1970
+ const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
1971
+ if (nonceResult) return nonceResult;
1880
1972
  return {
1881
1973
  ok: true,
1882
1974
  reason: "ok",
@@ -1986,14 +2078,8 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
1986
2078
  reason: "bootstrap_grant_invalid_signature",
1987
2079
  deviceId
1988
2080
  };
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);
2081
+ const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
2082
+ if (nonceResult) return nonceResult;
1997
2083
  return {
1998
2084
  ok: true,
1999
2085
  reason: "ok",
@@ -2367,6 +2453,21 @@ function recentScopeRejectionsByPeer(store) {
2367
2453
  }
2368
2454
  return map;
2369
2455
  }
2456
+ function legacySharedReviewSummary(store) {
2457
+ const row = store.db.prepare(`SELECT COUNT(*) AS memory_count,
2458
+ MAX(updated_at) AS last_updated_at
2459
+ FROM memory_items
2460
+ WHERE scope_id = ?
2461
+ AND active = 1
2462
+ AND deleted_at IS NULL`).get(LEGACY_SHARED_REVIEW_SCOPE_ID);
2463
+ const memoryCount = Number(row?.memory_count ?? 0);
2464
+ return {
2465
+ scope_id: LEGACY_SHARED_REVIEW_SCOPE_ID,
2466
+ memory_count: memoryCount,
2467
+ has_data: memoryCount > 0,
2468
+ last_updated_at: row?.last_updated_at ?? null
2469
+ };
2470
+ }
2370
2471
  var SYNC_DIRECTION_WINDOW_SECONDS = 1440 * 60;
2371
2472
  function recentPeerOps(store) {
2372
2473
  const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_DIRECTION_WINDOW_SECONDS * 1e3)).toISOString();
@@ -2438,7 +2539,8 @@ var PEERS_QUERY = `
2438
2539
  SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
2439
2540
  p.last_seen_at, p.last_sync_at, p.last_error,
2440
2541
  p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,
2441
- p.actor_id, a.display_name AS actor_display_name
2542
+ p.actor_id, p.discovered_via_coordinator_id, p.discovered_via_group_id,
2543
+ a.display_name AS actor_display_name
2442
2544
  FROM sync_peers AS p
2443
2545
  LEFT JOIN actors AS a ON a.actor_id = p.actor_id
2444
2546
  ORDER BY name, peer_device_id
@@ -2468,6 +2570,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
2468
2570
  const store = getStore();
2469
2571
  return (async () => {
2470
2572
  let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
2573
+ if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
2471
2574
  let preauthChecked = false;
2472
2575
  let bootstrapAttempted = false;
2473
2576
  if (!auth.ok) {
@@ -2483,6 +2586,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
2483
2586
  auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
2484
2587
  bootstrapAttempted = true;
2485
2588
  }
2589
+ if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
2486
2590
  if (!auth.ok) {
2487
2591
  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
2592
  const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
@@ -2508,6 +2612,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
2508
2612
  app.get("/v1/ops", (c) => {
2509
2613
  const store = getStore();
2510
2614
  const auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
2615
+ if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
2511
2616
  if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason), 401);
2512
2617
  const limited = rateLimitedResponse(c, auth.deviceId, true);
2513
2618
  if (limited) return limited;
@@ -2560,6 +2665,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
2560
2665
  const store = getStore();
2561
2666
  return (async () => {
2562
2667
  let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
2668
+ if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
2563
2669
  let preauthChecked = false;
2564
2670
  let bootstrapAttempted = false;
2565
2671
  if (!auth.ok) {
@@ -2575,6 +2681,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
2575
2681
  auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
2576
2682
  bootstrapAttempted = true;
2577
2683
  }
2684
+ if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
2578
2685
  if (!auth.ok) {
2579
2686
  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
2687
  const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
@@ -2626,6 +2733,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
2626
2733
  const raw = await readBoundedRequestBytes(c.req.raw, MAX_SYNC_BODY_BYTES);
2627
2734
  if (raw == null) return c.json({ error: "payload_too_large" }, 413);
2628
2735
  const auth = authorizeSyncRequest(store, c.req, raw);
2736
+ if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
2629
2737
  if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason), 401);
2630
2738
  const limited = rateLimitedResponse(c, auth.deviceId, true);
2631
2739
  if (limited) return limited;
@@ -2817,6 +2925,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2817
2925
  ping: {}
2818
2926
  };
2819
2927
  const legacyDevices = traceSync("legacyDevices", () => store.claimableLegacyDeviceIds());
2928
+ const legacyReview = traceSync("legacySharedReview", () => legacySharedReviewSummary(store));
2820
2929
  const sharingReview = traceSync("sharingReview", () => store.sharingReviewSummary(project));
2821
2930
  let joinRequests = [];
2822
2931
  if (includeJoinRequests && showDiag && config.syncCoordinatorAdminSecret) try {
@@ -2851,6 +2960,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2851
2960
  peers: peersItems,
2852
2961
  attempts: attemptsItems.slice(0, 5),
2853
2962
  legacy_devices: legacyDevices,
2963
+ legacy_shared_review: legacyReview,
2854
2964
  sharing_review: sharingReview,
2855
2965
  coordinator
2856
2966
  };