@codemem/server 0.33.0 → 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 +117 -5
- package/dist/index.js.map +1 -1
- package/dist/routes/stats.d.ts.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/package.json +2 -2
- package/static/app.js +42 -6
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
|
-
|
|
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;
|
|
@@ -2862,7 +2965,7 @@ function legacySharedReviewReassignmentPreview(store, input) {
|
|
|
2862
2965
|
scope_id: input.scopeId,
|
|
2863
2966
|
skipped_memory_count: rows.length - reassignableRows.length,
|
|
2864
2967
|
target_scope_label: targetScope.label || targetScope.scope_id,
|
|
2865
|
-
warning: "This
|
|
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.",
|
|
2866
2969
|
workspace_identity: input.workspaceIdentity
|
|
2867
2970
|
};
|
|
2868
2971
|
}
|
|
@@ -3319,6 +3422,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3319
3422
|
include: config.syncProjectsInclude,
|
|
3320
3423
|
exclude: config.syncProjectsExclude
|
|
3321
3424
|
},
|
|
3425
|
+
cleanup_diagnostics: cleanupDiagnostics(store, deviceRow?.device_id ?? null, showDiag),
|
|
3322
3426
|
redacted: !showDiag
|
|
3323
3427
|
};
|
|
3324
3428
|
if (showDiag) {
|
|
@@ -3725,7 +3829,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3725
3829
|
guardrail_warnings: analysis.warnings
|
|
3726
3830
|
}, 409);
|
|
3727
3831
|
}
|
|
3728
|
-
const
|
|
3832
|
+
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
3833
|
+
const mapping = upsertProjectScopeSettingsMapping(store.db, {
|
|
3834
|
+
deviceId,
|
|
3835
|
+
...mappingInput
|
|
3836
|
+
});
|
|
3729
3837
|
return c.json({
|
|
3730
3838
|
ok: true,
|
|
3731
3839
|
mapping,
|
|
@@ -3764,7 +3872,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3764
3872
|
guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
|
|
3765
3873
|
}, 409);
|
|
3766
3874
|
}
|
|
3767
|
-
const
|
|
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
|
+
})));
|
|
3768
3880
|
return c.json({
|
|
3769
3881
|
ok: true,
|
|
3770
3882
|
mappings: saveMappings(),
|