@codemem/server 0.36.0 → 0.37.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAiB,OAAO,EAAE,MAAM,eAAe,CAAC;AAE1F,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAEN,KAAK,0BAA0B,EAC/B,MAAM,yBAAyB,CAAC;AAQjC,OAAO,EAAE,OAAO,EAAE,CAAC;AAKnB,uFAAuF;AACvF,wBAAgB,QAAQ,IAAI,WAAW,CAKtC;AAED,mDAAmD;AACnD,wBAAgB,UAAU,IAAI,IAAI,CAGjC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B,YAAY,CAAC,EAAE,MAAM,WAAW,CAAC;IACjC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACjC,oBAAoB,CAAC,EAAE;QACtB,OAAO,CAAC,EAAE,0BAA0B,CAAC;QACrC,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,wBAAwB,CAAC,EAAE,MAAM,CAAC;QAClC,4BAA4B,CAAC,EAAE,MAAM,CAAC;KACtC,CAAC;IACF,oBAAoB,CAAC,EAAE,MAAM;QAC5B,KAAK,EACF,UAAU,GACV,SAAS,GACT,UAAU,GACV,OAAO,GACP,UAAU,GACV,iBAAiB,GACjB,iBAAiB,GACjB,IAAI,CAAC;QACR,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,GAAG,IAAI,CAAC;CACT;AAED,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,UAAU,8EA6D1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,UAAU,8EA8B9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAiB,OAAO,EAAE,MAAM,eAAe,CAAC;AAE1F,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAEN,KAAK,0BAA0B,EAC/B,MAAM,yBAAyB,CAAC;AAQjC,OAAO,EAAE,OAAO,EAAE,CAAC;AAKnB,uFAAuF;AACvF,wBAAgB,QAAQ,IAAI,WAAW,CAKtC;AAED,mDAAmD;AACnD,wBAAgB,UAAU,IAAI,IAAI,CAGjC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B,YAAY,CAAC,EAAE,MAAM,WAAW,CAAC;IACjC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACjC,oBAAoB,CAAC,EAAE;QACtB,OAAO,CAAC,EAAE,0BAA0B,CAAC;QACrC,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,wBAAwB,CAAC,EAAE,MAAM,CAAC;QAClC,4BAA4B,CAAC,EAAE,MAAM,CAAC;KACtC,CAAC;IACF,oBAAoB,CAAC,EAAE,MAAM;QAC5B,KAAK,EACF,UAAU,GACV,SAAS,GACT,UAAU,GACV,OAAO,GACP,UAAU,GACV,iBAAiB,GACjB,iBAAiB,GACjB,IAAI,CAAC;QACR,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,GAAG,IAAI,CAAC;CACT;AAED,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,UAAU,8EA8D1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,UAAU,8EA8B9C"}
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
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, buildRawEventEnvelopeFromCodexHook, 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";
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, buildRawEventEnvelopeFromCodexHook, 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, stripPrivateObj, 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";
7
7
  import { RateLimiter } from "limiter";
8
- import { homedir, networkInterfaces } from "node:os";
9
8
  import { and, count, desc, eq, inArray, max, ne } from "drizzle-orm";
10
9
  import { drizzle } from "drizzle-orm/better-sqlite3";
11
10
  import { createHash, randomUUID } from "node:crypto";
11
+ import { networkInterfaces } from "node:os";
12
12
  //#region src/middleware.ts
13
13
  var LOOPBACK_HOSTS = new Set([
14
14
  "127.0.0.1",
@@ -252,27 +252,6 @@ var DEFAULTS = {
252
252
  function loadProviderOptions() {
253
253
  return listObserverProviderOptions();
254
254
  }
255
- function getConfigPath() {
256
- const envPath = process.env.CODEMEM_CONFIG;
257
- if (envPath) return envPath.replace(/^~/, homedir());
258
- const configDir = join(homedir(), ".config", "codemem");
259
- return [join(configDir, "config.json"), join(configDir, "config.jsonc")].find((p) => existsSync(p)) ?? join(configDir, "config.json");
260
- }
261
- function readConfigFile(configPath) {
262
- if (!existsSync(configPath)) return {};
263
- try {
264
- let text = readFileSync(configPath, "utf-8").trim();
265
- if (!text) return {};
266
- try {
267
- return JSON.parse(text);
268
- } catch {
269
- text = stripTrailingCommas(stripJsonComments(text));
270
- return JSON.parse(text);
271
- }
272
- } catch {
273
- return {};
274
- }
275
- }
276
255
  function withoutRemovedConfigKeys(configData) {
277
256
  const next = { ...configData };
278
257
  for (const key of REMOVED_CONFIG_KEYS) delete next[key];
@@ -448,8 +427,8 @@ function applyRuntimeEffects(changedKeys, opts) {
448
427
  function configRoutes(opts = {}) {
449
428
  const app = new Hono();
450
429
  app.get("/api/config", (c) => {
451
- const configPath = getConfigPath();
452
- const configData = withoutRemovedConfigKeys(readConfigFile(configPath));
430
+ const configPath = getCodememConfigPath();
431
+ const configData = withoutRemovedConfigKeys(readCodememConfigFile());
453
432
  const effective = getEffectiveConfig(configData);
454
433
  return c.json({
455
434
  path: configPath,
@@ -556,37 +535,14 @@ function queryBool(value) {
556
535
  }
557
536
  //#endregion
558
537
  //#region src/routes/memory.ts
559
- function cleanOwnershipValue(value) {
560
- if (typeof value !== "string") return null;
561
- const trimmed = value.trim();
562
- return trimmed ? trimmed : null;
563
- }
564
- function buildOwnershipContext(store) {
565
- const claimedPeerIds = new Set(store.sameActorPeerIds());
566
- return {
567
- actorId: store.actorId,
568
- claimedPeerIds,
569
- deviceId: store.deviceId,
570
- legacyActorIds: new Set(Array.from(claimedPeerIds, (peerId) => `legacy-sync:${peerId}`))
571
- };
572
- }
573
- function memoryOwnedBySelf(row, ownership, metadata = fromJson(row.metadata_json ?? null)) {
574
- const meta = metadata;
575
- const actorId = cleanOwnershipValue(row.actor_id) ?? cleanOwnershipValue(meta.actor_id);
576
- if (actorId === ownership.actorId) return true;
577
- const deviceId = cleanOwnershipValue(row.origin_device_id) ?? cleanOwnershipValue(meta.origin_device_id);
578
- if (deviceId === ownership.deviceId) return true;
579
- if (deviceId && ownership.claimedPeerIds.has(deviceId)) return true;
580
- if (actorId && ownership.legacyActorIds.has(actorId)) return true;
581
- return false;
582
- }
583
- function serializeMemoryRow(ownership, row) {
538
+ function serializeMemoryRow(ownedBySelf, row) {
584
539
  const metadata = fromJson(row.metadata_json ?? null);
540
+ const owned_by_self = ownedBySelf(row);
585
541
  return {
586
542
  ...row,
587
543
  kind: canonicalMemoryKind(row.kind ?? null, row.metadata_json),
588
544
  metadata_json: metadata,
589
- owned_by_self: memoryOwnedBySelf(row, ownership, metadata)
545
+ owned_by_self
590
546
  };
591
547
  }
592
548
  /**
@@ -642,11 +598,7 @@ function normalizeScope(raw) {
642
598
  if (value === "mine" || value === "theirs") return value;
643
599
  }
644
600
  function buildViewerMemoryFilters(store, filters) {
645
- return buildFilterClausesWithContext(filters, {
646
- actorId: store.actorId,
647
- deviceId: store.deviceId,
648
- enforceScopeVisibility: true
649
- });
601
+ return buildFilterClausesWithContext(filters, store.ownershipFilterContext());
650
602
  }
651
603
  function countVisibleMemoryRows(store, filters) {
652
604
  const filterResult = buildViewerMemoryFilters(store, filters);
@@ -729,8 +681,8 @@ function queryMemoryPage(store, options) {
729
681
  WHERE ${where}
730
682
  ORDER BY memory_items.created_at DESC
731
683
  LIMIT ? OFFSET ?`).all(...filterResult.params, options.limit + 1, options.offset);
732
- const ownership = buildOwnershipContext(store);
733
- return rows.map((row) => serializeMemoryRow(ownership, row));
684
+ const ownedBySelf = store.buildOwnershipPredicate();
685
+ return rows.map((row) => serializeMemoryRow(ownedBySelf, row));
734
686
  }
735
687
  function isSummaryLikeMemory$1(item) {
736
688
  return isSummaryLikeMemory({
@@ -1010,7 +962,7 @@ function memoryRoutes(getStore) {
1010
962
  if (!store.get(memoryId)) return c.json({ error: "memory not found" }, 404);
1011
963
  const row = drizzle(store.db, { schema }).select().from(schema.memoryItems).where(eq(schema.memoryItems.id, memoryId)).get();
1012
964
  if (!row) return c.json({ error: "memory not found" }, 404);
1013
- if (!memoryOwnedBySelf(row, buildOwnershipContext(store))) return c.json({ error: "memory not owned by this device" }, 403);
965
+ if (!store.memoryOwnedBySelf(row)) return c.json({ error: "memory not owned by this device" }, 403);
1014
966
  if (Number(row.active ?? 1) === 0 || row.deleted_at != null) return c.json({ status: "ok" });
1015
967
  try {
1016
968
  store.forget(memoryId);
@@ -1536,30 +1488,29 @@ function sanitizePackUsageMetadata(metadata, visibility) {
1536
1488
  for (const key of MEMORY_ID_METADATA_KEYS) if (Array.isArray(sanitized[key])) sanitized[key] = metadataMemoryIds(sanitized[key]).filter((memoryId) => visibility.visibleMemoryIds.has(memoryId));
1537
1489
  return sanitized;
1538
1490
  }
1539
- function summarizeUsageEvents(rows) {
1540
- const byEvent = /* @__PURE__ */ new Map();
1541
- for (const row of rows) {
1542
- const summary = byEvent.get(row.event) ?? {
1543
- event: row.event,
1544
- total_tokens_read: 0,
1545
- total_tokens_written: 0,
1546
- total_tokens_saved: 0,
1547
- count: 0
1548
- };
1549
- summary.total_tokens_read += row.tokens_read;
1550
- summary.total_tokens_written += row.tokens_written;
1551
- summary.total_tokens_saved += row.tokens_saved;
1552
- summary.count += 1;
1553
- byEvent.set(row.event, summary);
1554
- }
1555
- return [...byEvent.values()].sort((a, b) => a.event.localeCompare(b.event));
1491
+ /**
1492
+ * Convert the neutral core aggregate rows into the wire shape for /api/usage
1493
+ * (ApiUsageEventSummary: total_tokens_* names), sorted by event name ASC to
1494
+ * match the route's historical ordering.
1495
+ */
1496
+ function mapAggregateEvents(rows) {
1497
+ return rows.map((row) => ({
1498
+ event: row.event,
1499
+ total_tokens_read: row.tokens_read,
1500
+ total_tokens_written: row.tokens_written,
1501
+ total_tokens_saved: row.tokens_saved,
1502
+ count: row.count
1503
+ })).sort((a, b) => a.event.localeCompare(b.event));
1556
1504
  }
1557
- function totalUsageEvents(rows) {
1505
+ /**
1506
+ * Sum the neutral core aggregate rows into the ApiUsageTotals wire shape.
1507
+ */
1508
+ function totalsFromAggregate(rows) {
1558
1509
  return rows.reduce((acc, row) => ({
1559
- tokens_read: Number(acc.tokens_read) + row.tokens_read,
1560
- tokens_written: Number(acc.tokens_written) + row.tokens_written,
1561
- tokens_saved: Number(acc.tokens_saved) + row.tokens_saved,
1562
- count: Number(acc.count) + 1
1510
+ tokens_read: acc.tokens_read + row.tokens_read,
1511
+ tokens_written: acc.tokens_written + row.tokens_written,
1512
+ tokens_saved: acc.tokens_saved + row.tokens_saved,
1513
+ count: acc.count + row.count
1563
1514
  }), {
1564
1515
  tokens_read: 0,
1565
1516
  tokens_written: 0,
@@ -1571,16 +1522,48 @@ function totalUsageEvents(rows) {
1571
1522
  * Short-lived cache for the computed /api/usage payload.
1572
1523
  *
1573
1524
  * The health dot polls /api/usage on every 5s refresh regardless of the
1574
- * active tab, and the computation scans the full usage_events table to apply
1575
- * scope visibility before aggregating. The resulting token totals are
1576
- * cumulative dashboard figures, so a few seconds of staleness is acceptable
1577
- * in exchange for collapsing the repeated full-table scan on the polling
1578
- * loop. Keyed by db path + scope identity + project filter so distinct
1579
- * stores/projects never share an entry.
1525
+ * active tab. The token/event totals come from indexed SQL aggregates and the
1526
+ * recent-pack window runs scope-visibility resolution over its bounded set, so
1527
+ * each miss is cheap — but the totals are cumulative dashboard figures where a
1528
+ * few seconds of staleness is acceptable, so caching still collapses the
1529
+ * repeated recompute on the polling loop. Keyed by db path + scope identity +
1530
+ * project filter so distinct stores/projects never share an entry.
1580
1531
  */
1581
1532
  var usagePayloadCache = /* @__PURE__ */ new Map();
1582
1533
  var USAGE_PAYLOAD_CACHE_MS = 1e4;
1583
1534
  /**
1535
+ * Hard cap on cached usage payloads. The cache key folds in the arbitrary
1536
+ * `?project=` filter and a scope-visibility fingerprint that becomes
1537
+ * per-request-unique on the error/bypass path, so without a bound the map
1538
+ * would grow without limit in a long-running viewer. The cap is far above the
1539
+ * realistic working set (a handful of projects times one or two scope
1540
+ * generations), so legitimate hits are never evicted under normal load.
1541
+ */
1542
+ var USAGE_PAYLOAD_CACHE_MAX_ENTRIES = 256;
1543
+ /**
1544
+ * Sweep expired entries and enforce the size cap before inserting a new one.
1545
+ *
1546
+ * Mirrors the lazy-sweep approach in request-rate-limit.ts: eviction is
1547
+ * piggybacked on writes rather than run on a timer, so an idle viewer does no
1548
+ * background work. Expired entries (those past `expiresAtMs`) are always
1549
+ * dropped; if the map is still at capacity afterward — e.g. a burst of
1550
+ * distinct, still-live bypass keys — the oldest-expiring entries are evicted
1551
+ * to make room. This never changes hit/miss semantics for a live entry: the
1552
+ * caller still re-checks `expiresAtMs` on read, and only already-expired or
1553
+ * over-cap entries are removed here.
1554
+ */
1555
+ function sweepUsagePayloadCache(cache, nowMs) {
1556
+ for (const [key, entry] of cache) if (entry.expiresAtMs <= nowMs) cache.delete(key);
1557
+ if (cache.size < USAGE_PAYLOAD_CACHE_MAX_ENTRIES) return;
1558
+ const byExpiry = [...cache.entries()].sort((a, b) => a[1].expiresAtMs - b[1].expiresAtMs);
1559
+ let overflow = cache.size - USAGE_PAYLOAD_CACHE_MAX_ENTRIES + 1;
1560
+ for (const [key] of byExpiry) {
1561
+ if (overflow <= 0) break;
1562
+ cache.delete(key);
1563
+ overflow -= 1;
1564
+ }
1565
+ }
1566
+ /**
1584
1567
  * Cheap fingerprint of the state that drives scope visibility
1585
1568
  * (`scope_memberships` / `replication_scopes`). Folding this into the usage
1586
1569
  * cache key ensures a membership/scope status change — e.g. a revocation —
@@ -1670,30 +1653,31 @@ function statsRoutes(getStore) {
1670
1653
  const nowMs = Date.now();
1671
1654
  const cached = usagePayloadCache.get(cacheKey);
1672
1655
  if (cached && nowMs < cached.expiresAtMs) return c.json(cached.payload);
1673
- const loadUsageRows = (project) => {
1656
+ const globalAggregate = store.usageAggregate();
1657
+ const eventsGlobal = mapAggregateEvents(globalAggregate);
1658
+ const totalsGlobal = totalsFromAggregate(globalAggregate);
1659
+ const filteredAggregate = projectFilter ? store.usageAggregate(projectFilter) : null;
1660
+ const eventsFiltered = filteredAggregate ? mapAggregateEvents(filteredAggregate) : null;
1661
+ const totalsFiltered = filteredAggregate ? totalsFromAggregate(filteredAggregate) : null;
1662
+ const RECENT_PACK_SCAN_LIMIT = 200;
1663
+ const loadRecentPackWindow = (project) => {
1674
1664
  return (project ? store.db.prepare(`SELECT usage_events.id, usage_events.session_id, usage_events.event,
1675
1665
  usage_events.tokens_read, usage_events.tokens_written, usage_events.tokens_saved,
1676
1666
  usage_events.created_at, usage_events.metadata_json
1677
1667
  FROM usage_events
1678
1668
  JOIN sessions ON sessions.id = usage_events.session_id
1679
- WHERE sessions.project = ?
1680
- ORDER BY usage_events.created_at DESC`).all(project) : store.db.prepare(`SELECT id, session_id, event, tokens_read, tokens_written, tokens_saved,
1669
+ WHERE sessions.project = ? AND usage_events.event = 'pack'
1670
+ ORDER BY usage_events.created_at DESC
1671
+ LIMIT ?`).all(project, RECENT_PACK_SCAN_LIMIT) : store.db.prepare(`SELECT id, session_id, event, tokens_read, tokens_written, tokens_saved,
1681
1672
  created_at, metadata_json
1682
1673
  FROM usage_events
1683
- ORDER BY created_at DESC`).all()).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json)));
1674
+ WHERE event = 'pack'
1675
+ ORDER BY created_at DESC
1676
+ LIMIT ?`).all(RECENT_PACK_SCAN_LIMIT)).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json)));
1684
1677
  };
1685
- const loadedGlobalRows = loadUsageRows();
1686
- const globalVisibility = buildUsageVisibility(store, loadedGlobalRows);
1687
- const globalRows = loadedGlobalRows.filter((row) => usageRowVisible(row, globalVisibility));
1688
- const loadedFilteredRows = projectFilter ? loadUsageRows(projectFilter) : null;
1689
- const filteredVisibility = loadedFilteredRows ? buildUsageVisibility(store, loadedFilteredRows) : null;
1690
- const filteredRows = loadedFilteredRows ? loadedFilteredRows.filter((row) => usageRowVisible(row, filteredVisibility ?? globalVisibility)) : null;
1691
- const eventsGlobal = summarizeUsageEvents(globalRows);
1692
- const totalsGlobal = totalUsageEvents(globalRows);
1693
- const eventsFiltered = filteredRows ? summarizeUsageEvents(filteredRows) : null;
1694
- const totalsFiltered = filteredRows ? totalUsageEvents(filteredRows) : null;
1695
- const recentVisibility = filteredVisibility ?? globalVisibility;
1696
- const recentPacks = (filteredRows ?? globalRows).filter((row) => row.event === "pack").slice(0, 10).map((row) => ({
1678
+ const recentWindow = loadRecentPackWindow(projectFilter);
1679
+ const recentVisibility = buildUsageVisibility(store, recentWindow);
1680
+ const recentPacks = recentWindow.filter((row) => usageRowVisible(row, recentVisibility)).slice(0, 10).map((row) => ({
1697
1681
  ...row,
1698
1682
  metadata_json: sanitizePackUsageMetadata(row.metadata_json, recentVisibility)
1699
1683
  }));
@@ -1707,9 +1691,11 @@ function statsRoutes(getStore) {
1707
1691
  totals_filtered: totalsFiltered,
1708
1692
  recent_packs: recentPacks
1709
1693
  };
1694
+ const setAtMs = Date.now();
1695
+ sweepUsagePayloadCache(usagePayloadCache, setAtMs);
1710
1696
  usagePayloadCache.set(cacheKey, {
1711
1697
  payload,
1712
- expiresAtMs: Date.now() + USAGE_PAYLOAD_CACHE_MS
1698
+ expiresAtMs: setAtMs + USAGE_PAYLOAD_CACHE_MS
1713
1699
  });
1714
1700
  return c.json(payload);
1715
1701
  }
@@ -3677,7 +3663,12 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3677
3663
  const store = getStore();
3678
3664
  {
3679
3665
  const d = drizzle(store.db, { schema });
3680
- const body = await c.req.json();
3666
+ let body;
3667
+ try {
3668
+ body = await c.req.json();
3669
+ } catch {
3670
+ return c.json({ error: "invalid json" }, 400);
3671
+ }
3681
3672
  const peerDeviceId = String(body.peer_device_id ?? "").trim();
3682
3673
  const name = String(body.name ?? "").trim();
3683
3674
  if (!peerDeviceId) return c.json({ error: "peer_device_id required" }, 400);
@@ -5487,7 +5478,8 @@ function createApp(opts) {
5487
5478
  });
5488
5479
  app.use("/assets/*", serveStatic({
5489
5480
  root: staticRoot,
5490
- rewriteRequestPath: (path) => path.replace(/^\/assets/, "")
5481
+ rewriteRequestPath: (path) => path.replace(/^\/assets/, ""),
5482
+ precompressed: true
5491
5483
  }));
5492
5484
  const indexPath = join(staticRoot, "index.html");
5493
5485
  if (!existsSync(indexPath)) throw new Error(`Viewer assets missing at ${indexPath}. Run \`pnpm build\` from the repo root before starting the viewer.`);