@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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -106
- package/dist/index.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/stats.d.ts +29 -0
- package/dist/routes/stats.d.ts.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/package.json +4 -4
- package/static/app.js +112 -22500
- package/static/app.js.br +0 -0
- package/static/app.js.gz +0 -0
- package/static/index.html +0 -1
- package/static/themes.css.br +0 -0
- package/static/themes.css.gz +0 -0
- package/static/tokens.css.br +0 -0
- package/static/tokens.css.gz +0 -0
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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,
|
|
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 =
|
|
452
|
-
const configData = withoutRemovedConfigKeys(
|
|
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
|
|
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
|
|
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
|
|
733
|
-
return rows.map((row) => serializeMemoryRow(
|
|
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
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
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:
|
|
1560
|
-
tokens_written:
|
|
1561
|
-
tokens_saved:
|
|
1562
|
-
count:
|
|
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
|
|
1575
|
-
* scope
|
|
1576
|
-
*
|
|
1577
|
-
*
|
|
1578
|
-
* loop. Keyed by db path + scope identity +
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1686
|
-
const
|
|
1687
|
-
const
|
|
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:
|
|
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
|
-
|
|
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.`);
|