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:
parent
18cc8218b2
commit
2bc903d128
2 changed files with 50 additions and 9 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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("");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue