feat(cvm-server): heartbeat re-announce, wire LOG_LEVEL, fix from_date

- Heartbeat: re-publish discovery announcements every CVM_HEARTBEAT_MS (15 min)
  via the SDK's announcement manager (defensively reached), so recency-based
  health checks stay green for a long-running process.
- LOG_LEVEL is now actually passed to the transport (was documented but unused).
- find_events: from_date was a no-op (filter(() => true)); now filters by the
  kind-31923 start tag.
- .env.example: fix CVM_RELAY (.org), drop the dead wotr relay, document
  CVM_HEARTBEAT_MS.
This commit is contained in:
SkateSpots 2026-09-27 10:32:07 +07:00
parent 18cc8218b2
commit 2bc903d128
2 changed files with 50 additions and 9 deletions

View file

@ -7,11 +7,15 @@
# NEVER commit this to git! # NEVER commit this to git!
CVM_SERVER_NSEC_HEX=your_64_char_hex_private_key_here CVM_SERVER_NSEC_HEX=your_64_char_hex_private_key_here
# Nostr relays to query for skate data # Nostr relays to query for skate data (comma-separated)
NOSTR_RELAYS=wss://goskatespots.nostr1.com,wss://relay.ditto.pub,wss://wotr.relatr.xyz NOSTR_RELAYS=wss://goskatespots.nostr1.com,wss://relay.ditto.pub
# CVM discovery relay — where agents find your server # CVM discovery relay — where agents find your server
CVM_RELAY=wss://relay.contextvm.net CVM_RELAY=wss://relay.contextvm.org
# Log level: info | debug | warn | error # Log level passed to the ContextVM transport: debug | info | warn | error | silent
LOG_LEVEL=info LOG_LEVEL=info
# How often to re-publish the discovery announcements, in ms (0 disables).
# Keeps recency-based health checks green between (re)starts.
CVM_HEARTBEAT_MS=900000

View file

@ -44,6 +44,16 @@ const NOSTR_RELAYS = (process.env.NOSTR_RELAYS || "wss://goskatespots.nostr1.com
const CVM_RELAY = process.env.CVM_RELAY || "wss://relay.contextvm.org"; const CVM_RELAY = process.env.CVM_RELAY || "wss://relay.contextvm.org";
// Transport log level. Previously documented but never consumed (always the
// SDK default); now actually wired through to the transport.
type SdkLogLevel = "debug" | "info" | "warn" | "error" | "silent";
const LOG_LEVEL = (process.env.LOG_LEVEL || "info") as SdkLogLevel;
// How often to re-publish the server's discovery announcements. The SDK only
// announces on start(), so without this a long-running server looks "stale" to
// recency-based health checks. Set 0 to disable.
const HEARTBEAT_MS = Number(process.env.CVM_HEARTBEAT_MS || 15 * 60 * 1000);
// ─── Nostr data pool ───────────────────────────────────────────────────────── // ─── Nostr data pool ─────────────────────────────────────────────────────────
// Used to query skate data from the community relays // Used to query skate data from the community relays
@ -303,11 +313,13 @@ mcpServer.registerTool(
if (from_date) { if (from_date) {
const fromTs = Math.floor(new Date(from_date).getTime() / 1000); const fromTs = Math.floor(new Date(from_date).getTime() / 1000);
events = events.filter(() => { if (Number.isFinite(fromTs)) {
// start tag is unix timestamp for kind 31923 // kind 31923 stores its start as a unix timestamp in the `start` tag.
return true; // already filtered by since: now above events = events.filter((e) => {
}); const start = parseInt(getTag(e, "start") ?? "0");
void fromTs; return !start || start >= fromTs;
});
}
} }
if (to_date) { if (to_date) {
@ -606,6 +618,7 @@ async function main() {
signer, signer,
relayHandler: cvmRelayPool, relayHandler: cvmRelayPool,
isAnnouncedServer: true, isAnnouncedServer: true,
logLevel: LOG_LEVEL,
serverInfo: { serverInfo: {
name: "SkateSpots Data Server", name: "SkateSpots Data Server",
about: about:
@ -617,6 +630,30 @@ async function main() {
await mcpServer.connect(transport); await mcpServer.connect(transport);
// ─── Heartbeat ─────────────────────────────────────────────────────────────
// Re-publish the server announcements periodically so recency-based health
// checks stay green for a long-running process. The announcement manager is a
// typed-private SDK member, so we reach it defensively: if a future SDK
// renames it we log and skip (health checks then fall back to restart-cadence
// freshness) instead of crashing the server.
const announcementManager = (
transport as unknown as {
announcementManager?: { publishPublicAnnouncements?: () => Promise<void> };
}
).announcementManager;
if (HEARTBEAT_MS > 0 && typeof announcementManager?.publishPublicAnnouncements === "function") {
const heartbeat = setInterval(() => {
announcementManager.publishPublicAnnouncements!().catch((e) =>
console.error("[heartbeat] re-announce failed:", e)
);
}, HEARTBEAT_MS);
(heartbeat as { unref?: () => void }).unref?.();
console.log(` Heartbeat : every ${Math.round(HEARTBEAT_MS / 60000)} min`);
} else if (HEARTBEAT_MS > 0) {
console.warn(" ⚠️ Heartbeat unavailable — SDK internals changed; health checks may show stale.");
}
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
console.log(" ✅ Server running — listening for agents on Nostr"); console.log(" ✅ Server running — listening for agents on Nostr");
console.log(""); console.log("");