From 2bc903d128c8b980a728869f0f193bb122363e72 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sun, 27 Sep 2026 10:32:07 +0700 Subject: [PATCH] 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. --- cvm-server/.env.example | 12 +++++++---- cvm-server/server.ts | 47 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/cvm-server/.env.example b/cvm-server/.env.example index 39886cc..9c19bd9 100644 --- a/cvm-server/.env.example +++ b/cvm-server/.env.example @@ -7,11 +7,15 @@ # NEVER commit this to git! CVM_SERVER_NSEC_HEX=your_64_char_hex_private_key_here -# Nostr relays to query for skate data -NOSTR_RELAYS=wss://goskatespots.nostr1.com,wss://relay.ditto.pub,wss://wotr.relatr.xyz +# Nostr relays to query for skate data (comma-separated) +NOSTR_RELAYS=wss://goskatespots.nostr1.com,wss://relay.ditto.pub # 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 + +# 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 diff --git a/cvm-server/server.ts b/cvm-server/server.ts index 5b55541..38a1f97 100644 --- a/cvm-server/server.ts +++ b/cvm-server/server.ts @@ -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"; +// 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 ───────────────────────────────────────────────────────── // Used to query skate data from the community relays @@ -303,11 +313,13 @@ mcpServer.registerTool( if (from_date) { const fromTs = Math.floor(new Date(from_date).getTime() / 1000); - events = events.filter(() => { - // start tag is unix timestamp for kind 31923 - return true; // already filtered by since: now above - }); - void fromTs; + if (Number.isFinite(fromTs)) { + // kind 31923 stores its start as a unix timestamp in the `start` tag. + events = events.filter((e) => { + const start = parseInt(getTag(e, "start") ?? "0"); + return !start || start >= fromTs; + }); + } } if (to_date) { @@ -606,6 +618,7 @@ async function main() { signer, relayHandler: cvmRelayPool, isAnnouncedServer: true, + logLevel: LOG_LEVEL, serverInfo: { name: "SkateSpots Data Server", about: @@ -617,6 +630,30 @@ async function main() { 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 }; + } + ).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(" ✅ Server running — listening for agents on Nostr"); console.log("");