@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 +158 -6
- package/dist/index.js.map +1 -1
- package/dist/routes/raw-events.d.ts +1 -1
- package/dist/routes/raw-events.d.ts.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, 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(),
|