@codemem/server 0.33.0 → 0.35.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, 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";
4
4
  import { serveStatic } from "@hono/node-server/serve-static";
5
5
  import { Hono } from "hono";
6
6
  import { createMiddleware } from "hono/factory";
@@ -1090,7 +1090,7 @@ function observerStatusRoutes(deps) {
1090
1090
  //#region src/routes/raw-events.ts
1091
1091
  /**
1092
1092
  * Raw events routes — GET & POST /api/raw-events, GET /api/raw-events/status,
1093
- * POST /api/claude-hooks.
1093
+ * POST /api/claude-hooks, POST /api/codex-hooks.
1094
1094
  */
1095
1095
  var MAX_RAW_EVENTS_BODY_BYTES = Number.parseInt(process.env.CODEMEM_RAW_EVENTS_MAX_BODY_BYTES ?? "", 10) || 1048576;
1096
1096
  /** Keys to check (in priority order) when resolving a session stream id. */
@@ -1382,6 +1382,46 @@ function rawEventsRoutes(getStore, sweeper) {
1382
1382
  return c.json(response, 500);
1383
1383
  }
1384
1384
  });
1385
+ app.post("/api/codex-hooks", async (c) => {
1386
+ const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
1387
+ if (result instanceof Response) return result;
1388
+ const envelope = buildRawEventEnvelopeFromCodexHook(result);
1389
+ if (envelope === null) return c.json({
1390
+ inserted: 0,
1391
+ skipped: 1
1392
+ });
1393
+ const store = getStore();
1394
+ try {
1395
+ const opencodeSessionId = envelope.opencode_session_id;
1396
+ const source = envelope.source;
1397
+ const strippedPayload = stripPrivateObj(envelope.payload);
1398
+ const inserted = store.recordRawEvent({
1399
+ opencodeSessionId,
1400
+ source,
1401
+ eventId: envelope.event_id,
1402
+ eventType: envelope.event_type,
1403
+ payload: strippedPayload,
1404
+ tsWallMs: envelope.ts_wall_ms
1405
+ });
1406
+ store.updateRawEventSessionMeta({
1407
+ opencodeSessionId,
1408
+ source,
1409
+ cwd: envelope.cwd,
1410
+ project: envelope.project,
1411
+ startedAt: envelope.started_at,
1412
+ lastSeenTsWallMs: envelope.ts_wall_ms
1413
+ });
1414
+ nudgeSweeper(sweeper, [opencodeSessionId], source);
1415
+ return c.json({
1416
+ inserted: inserted ? 1 : 0,
1417
+ skipped: 0
1418
+ });
1419
+ } catch (err) {
1420
+ const response = { error: "internal server error" };
1421
+ if (process.env.CODEMEM_VIEWER_DEBUG === "1") response.detail = err.message;
1422
+ return c.json(response, 500);
1423
+ }
1424
+ });
1385
1425
  return app;
1386
1426
  }
1387
1427
  //#endregion
@@ -1528,6 +1568,57 @@ function totalUsageEvents(rows) {
1528
1568
  });
1529
1569
  }
1530
1570
  /**
1571
+ * Short-lived cache for the computed /api/usage payload.
1572
+ *
1573
+ * 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.
1580
+ */
1581
+ var usagePayloadCache = /* @__PURE__ */ new Map();
1582
+ var USAGE_PAYLOAD_CACHE_MS = 1e4;
1583
+ /**
1584
+ * Cheap fingerprint of the state that drives scope visibility
1585
+ * (`scope_memberships` / `replication_scopes`). Folding this into the usage
1586
+ * cache key ensures a membership/scope status change — e.g. a revocation —
1587
+ * invalidates any cached payload immediately instead of letting a device keep
1588
+ * reading a scope it can no longer see for up to the TTL. These tables are
1589
+ * small, so the aggregate is negligible next to the usage_events scan the
1590
+ * cache exists to avoid. Fails safe: any error forces a unique value so the
1591
+ * request bypasses the cache and recomputes visibility from scratch.
1592
+ */
1593
+ function scopeVisibilityGeneration(store) {
1594
+ try {
1595
+ const row = store.db.prepare(`SELECT
1596
+ (SELECT COUNT(*) FROM scope_memberships) AS m_count,
1597
+ (SELECT COUNT(*) FROM scope_memberships WHERE status = 'active') AS m_active,
1598
+ (SELECT COALESCE(MAX(updated_at), '') FROM scope_memberships) AS m_updated,
1599
+ (SELECT COALESCE(MAX(membership_epoch), 0) FROM scope_memberships) AS m_epoch,
1600
+ (SELECT COUNT(*) FROM replication_scopes) AS s_count,
1601
+ (SELECT COUNT(*) FROM replication_scopes WHERE status = 'active') AS s_active,
1602
+ (SELECT COALESCE(MAX(updated_at), '') FROM replication_scopes) AS s_updated`).get();
1603
+ if (!row) return "no-scope-state";
1604
+ return [
1605
+ row.m_count,
1606
+ row.m_active,
1607
+ row.m_updated,
1608
+ row.m_epoch,
1609
+ row.s_count,
1610
+ row.s_active,
1611
+ row.s_updated
1612
+ ].join(":");
1613
+ } catch {
1614
+ return `bypass-${Date.now()}-${Math.random()}`;
1615
+ }
1616
+ }
1617
+ function usageCacheKey(store, projectFilter) {
1618
+ const visibilityGeneration = scopeVisibilityGeneration(store);
1619
+ return `${store.dbPath}|${store.actorId}|${store.deviceId}|${projectFilter ?? ""}|${visibilityGeneration}`;
1620
+ }
1621
+ /**
1531
1622
  * Create stats routes. The store factory is called per-request to get a
1532
1623
  * fresh connection (matching the Python viewer pattern).
1533
1624
  */
@@ -1575,6 +1666,10 @@ function statsRoutes(getStore) {
1575
1666
  const store = getStore();
1576
1667
  {
1577
1668
  const projectFilter = c.req.query("project") || null;
1669
+ const cacheKey = usageCacheKey(store, projectFilter);
1670
+ const nowMs = Date.now();
1671
+ const cached = usagePayloadCache.get(cacheKey);
1672
+ if (cached && nowMs < cached.expiresAtMs) return c.json(cached.payload);
1578
1673
  const loadUsageRows = (project) => {
1579
1674
  return (project ? store.db.prepare(`SELECT usage_events.id, usage_events.session_id, usage_events.event,
1580
1675
  usage_events.tokens_read, usage_events.tokens_written, usage_events.tokens_saved,
@@ -1602,7 +1697,7 @@ function statsRoutes(getStore) {
1602
1697
  ...row,
1603
1698
  metadata_json: sanitizePackUsageMetadata(row.metadata_json, recentVisibility)
1604
1699
  }));
1605
- return c.json({
1700
+ const payload = {
1606
1701
  project: projectFilter,
1607
1702
  events: projectFilter ? eventsFiltered : eventsGlobal,
1608
1703
  totals: projectFilter ? totalsFiltered : totalsGlobal,
@@ -1611,7 +1706,12 @@ function statsRoutes(getStore) {
1611
1706
  events_filtered: eventsFiltered,
1612
1707
  totals_filtered: totalsFiltered,
1613
1708
  recent_packs: recentPacks
1709
+ };
1710
+ usagePayloadCache.set(cacheKey, {
1711
+ payload,
1712
+ expiresAtMs: Date.now() + USAGE_PAYLOAD_CACHE_MS
1614
1713
  });
1714
+ return c.json(payload);
1615
1715
  }
1616
1716
  });
1617
1717
  return app;
@@ -2616,6 +2716,7 @@ function mapSyncAttemptRow(row, showDiag, addresses) {
2616
2716
  };
2617
2717
  }
2618
2718
  var SYNC_SCOPE_REJECTION_WINDOW_SECONDS = 1440 * 60;
2719
+ var CLEANUP_DIAGNOSTICS_MAX_ROWS = 250;
2619
2720
  function recentScopeRejectionsByPeer(store) {
2620
2721
  const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_SCOPE_REJECTION_WINDOW_SECONDS * 1e3)).toISOString();
2621
2722
  const summaries = summarizeInboundScopeRejections(store.db, { sinceIso: cutoff });
@@ -2627,6 +2728,48 @@ function recentScopeRejectionsByPeer(store) {
2627
2728
  }
2628
2729
  return map;
2629
2730
  }
2731
+ function cleanupDiagnostics(store, localDeviceId, showDiag) {
2732
+ const opRows = store.db.prepare(`SELECT
2733
+ SUM(CASE WHEN device_id = ? THEN 1 ELSE 0 END) AS source_authored,
2734
+ SUM(CASE WHEN device_id != ? THEN 1 ELSE 0 END) AS applied,
2735
+ MAX(created_at) AS latest_at
2736
+ FROM replication_ops
2737
+ WHERE op_type = ?`).get(localDeviceId ?? "", localDeviceId ?? "", ACCESS_CLEANUP_OP_TYPE);
2738
+ const stale = localDeviceId && showDiag ? diagnoseStalePeerReceivedRows(store.db, {
2739
+ localDeviceId,
2740
+ maxRows: CLEANUP_DIAGNOSTICS_MAX_ROWS
2741
+ }) : {
2742
+ ambiguous: [],
2743
+ checked: 0,
2744
+ retained: 0,
2745
+ would_delete: 0,
2746
+ would_delete_memory_ids: []
2747
+ };
2748
+ const ambiguousByReason = {};
2749
+ for (const item of stale.ambiguous) ambiguousByReason[item.reason] = (ambiguousByReason[item.reason] ?? 0) + 1;
2750
+ const sourceAuthored = Number(opRows?.source_authored ?? 0);
2751
+ const applied = Number(opRows?.applied ?? 0);
2752
+ const ambiguous = stale.ambiguous.length;
2753
+ return {
2754
+ state: stale.would_delete ? "cleanup_pending" : ambiguous ? "needs_review" : applied ? "cleanup_applied" : sourceAuthored ? "cleanup_announced" : "clear",
2755
+ access_cleanup_ops: {
2756
+ source_authored: sourceAuthored,
2757
+ applied,
2758
+ latest_at: opRows?.latest_at ?? null
2759
+ },
2760
+ stale_peer_rows: {
2761
+ checked: stale.checked,
2762
+ diagnostic_limit: showDiag ? CLEANUP_DIAGNOSTICS_MAX_ROWS : 0,
2763
+ scan_skipped: !showDiag || !localDeviceId,
2764
+ would_remove: stale.would_delete,
2765
+ ambiguous,
2766
+ ambiguous_by_reason: ambiguousByReason,
2767
+ retained: stale.retained,
2768
+ items: showDiag ? stale.ambiguous.slice(0, 25) : void 0
2769
+ },
2770
+ redacted: !showDiag
2771
+ };
2772
+ }
2630
2773
  function legacySharedReviewSummary(store) {
2631
2774
  const rows = legacySharedReviewRows(store);
2632
2775
  const memoryCount = rows.length;
@@ -2862,7 +3005,7 @@ function legacySharedReviewReassignmentPreview(store, input) {
2862
3005
  scope_id: input.scopeId,
2863
3006
  skipped_memory_count: rows.length - reassignableRows.length,
2864
3007
  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.",
3008
+ 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
3009
  workspace_identity: input.workspaceIdentity
2867
3010
  };
2868
3011
  }
@@ -3319,6 +3462,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3319
3462
  include: config.syncProjectsInclude,
3320
3463
  exclude: config.syncProjectsExclude
3321
3464
  },
3465
+ cleanup_diagnostics: cleanupDiagnostics(store, deviceRow?.device_id ?? null, showDiag),
3322
3466
  redacted: !showDiag
3323
3467
  };
3324
3468
  if (showDiag) {
@@ -3725,7 +3869,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3725
3869
  guardrail_warnings: analysis.warnings
3726
3870
  }, 409);
3727
3871
  }
3728
- const mapping = upsertProjectScopeSettingsMapping(store.db, { ...mappingInput });
3872
+ const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
3873
+ const mapping = upsertProjectScopeSettingsMapping(store.db, {
3874
+ deviceId,
3875
+ ...mappingInput
3876
+ });
3729
3877
  return c.json({
3730
3878
  ok: true,
3731
3879
  mapping,
@@ -3764,7 +3912,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3764
3912
  guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
3765
3913
  }, 409);
3766
3914
  }
3767
- const saveMappings = store.db.transaction(() => mappingInputs.map((mappingInput) => upsertProjectScopeSettingsMapping(store.db, mappingInput)));
3915
+ const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
3916
+ const saveMappings = store.db.transaction(() => mappingInputs.map((mappingInput) => upsertProjectScopeSettingsMapping(store.db, {
3917
+ deviceId,
3918
+ ...mappingInput
3919
+ })));
3768
3920
  return c.json({
3769
3921
  ok: true,
3770
3922
  mappings: saveMappings(),