diff --git a/bin/iak-mcp-daemon.mjs b/bin/iak-mcp-daemon.mjs index dd28709..c9552b1 100755 --- a/bin/iak-mcp-daemon.mjs +++ b/bin/iak-mcp-daemon.mjs @@ -84,8 +84,24 @@ if (!apiKey) { } else if (!room) { console.warn('[iak-mcp-daemon] mcp.confirmations.room missing — chat-reply poller disabled'); } else { + // Poll cadence is a COST decision, not a detail: at the old hard-wired 5000 ms + // this one poller made 17,280 requests a day per device, and the host bills per + // invocation (2026-09-12: the fleet's Vercel bill passed 200 USD/month, ~99% of + // it request volume). The adjacent poller.interval_sec key already existed in + // config and was silently ignored here. Default is unchanged so nobody's + // latency moves without them asking. + // Two different jobs share this one loop, and they want opposite things. + // Reading the room is paid for per request. Carrying petrus's /approve tap is + // the only interval in the fleet a HUMAN feels: 30 seconds after tapping + // Approve reads as broken, not thrifty (claudeMB on PR #101). So the gate gets + // its own key and falls back to the cheap one, then to the old default — which + // means nobody's latency moves unless they set something. + const pollerIntervalMs = Math.max(1000, Number( + config?.mcp?.confirmations?.interval_sec + ?? config?.poller?.interval_sec + ?? 5) * 1000); startChatReplyPoller({ - apiKey, room, intervalMs: 5000, + apiKey, room, intervalMs: pollerIntervalMs, // Exact owner identities allowed to settle intents (config, with the // fleet's known surfaces as the default). Every surface the owner taps // from must be listed — an unlisted one gets a VISIBLE rejection reply, @@ -93,7 +109,7 @@ if (!apiKey) { owners: config?.mcp?.confirmations?.owners || ['petrus', 'petrus-boox'], log: (msg) => console.log(`[iak-mcp-daemon] ${msg}`), }); - console.log(`[iak-mcp-daemon] chat-reply poller watching room "${room}" every 5s`); + console.log(`[iak-mcp-daemon] chat-reply poller watching room "${room}" every ${pollerIntervalMs / 1000}s`); // Mirror every intent/action transition to the central action_status store // (antfarm PR #43) so CodeWatch renders durable button state off-LAN. const pushBase = config?.groupmind?.base_url || config?.groupmind?.baseUrl || 'https://groupmind.one/api/v1'; diff --git a/scripts/claudemm-uik-daemon.sh b/scripts/claudemm-uik-daemon.sh index b8fc010..2a0040a 100755 --- a/scripts/claudemm-uik-daemon.sh +++ b/scripts/claudemm-uik-daemon.sh @@ -18,6 +18,11 @@ export INTENT_USER_ID="$(python3 -c "import json;print(json.load(open('$CONFIG') export INTENT_AGENT_HANDLE="@claudemm" export INTENT_DEVICE_ID="mac-mini" +# 2026-09-12: uik-daemon defaults to POLL_INTERVAL_MS=30000, i.e. 2,880 requests/day +# per device. With the fleet on the same cadence Vercel billing passed 200 USD/month. +# 120s still detects a dead agent inside two minutes, at a quarter of the traffic. +export POLL_INTERVAL_MS="${POLL_INTERVAL_MS:-120000}" + # launchd starts this with a bare PATH (no Homebrew): resolve node explicitly # or the job dies with "exec: node: not found" - which is what kept the # publisher an orphan started by hand instead of a KeepAlive service. diff --git a/scripts/mini-vitals.sh b/scripts/mini-vitals.sh index 87e1177..65c21c2 100755 --- a/scripts/mini-vitals.sh +++ b/scripts/mini-vitals.sh @@ -33,5 +33,9 @@ print(f'{total:.1f} {avail:.1f}')")" mem_total_gb="$MEMT" mem_available_gb="$MEMA" \ memory="${MEMT}GB total, ${MEMA}GB available" \ --config "$CONFIG" >/dev/null 2>&1 || true - sleep 30 + # 2026-09-12: was 30s = 2,880 API writes/day from this loop alone, and Vercel + # billing passed 200 USD/month on fleet request volume (petrus). Load and free + # memory in a dashboard row do not need 30-second freshness; 5 minutes keeps the + # row visibly live at a tenth of the requests. + sleep "${VITALS_INTERVAL_S:-300}" done diff --git a/src/confirmations.mjs b/src/confirmations.mjs index d7adaa7..9ca2362 100644 --- a/src/confirmations.mjs +++ b/src/confirmations.mjs @@ -1102,6 +1102,11 @@ export function composeAnnouncers(map) { // Ported from the Mini's field-hardened fork (branch mini-local-fork-rescue), // security-reviewed by codexmb 2026-08-27; `owner` kept as an alias so // existing call sites keep working. +// NOTE on `intervalMs`: this default is a COST decision as much as a latency one. +// The host bills per request, so 5000 ms is 17,280 requests/day/device for this +// poller alone — the single largest source of our 2026-09 hosting bill. It is kept +// at 5000 for backward compatibility, but callers should pass a value explicitly; +// iak-mcp-daemon.mjs derives one from mcp.confirmations.interval_sec. export function startChatReplyPoller({ apiKey, room, intervalMs = 5000, log, owners, owner = 'petrus' }) { if (!apiKey || !room) { process.stderr.write('[iak-mcp] chat-reply poller: missing apiKey or room — disabled\n');