From d64db053587d89321db86fc08bdb2e6532505179 Mon Sep 17 00:00:00 2001 From: claudemm Date: Sun, 13 Sep 2026 00:16:20 +0300 Subject: [PATCH 1/2] perf: stop three loops billing by the invocation The fleet's hosting bill is per request, and three loops on every device were tuned as if requests were free. - chat-reply poller: intervalMs was hard-wired to 5000, ignoring the poller.interval_sec key sitting next to it in the same config object. That is 17,280 requests/day/device from one poller. Now reads the config key; default unchanged at 5s so no one's approval latency moves without asking. - mini-vitals: 30s -> 300s. It republishes load and free memory into a dashboard row a human reads a few times a day. 2,880/day bought nothing. - presence daemon: exports POLL_INTERVAL_MS=120000 (was the 30000 default). Two minutes still catches a dead agent. Both scripts take an env override so tuning needs no edit. Co-Authored-By: Claude Opus 5 --- bin/iak-mcp-daemon.mjs | 11 +++++++++-- scripts/claudemm-uik-daemon.sh | 5 +++++ scripts/mini-vitals.sh | 6 +++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/bin/iak-mcp-daemon.mjs b/bin/iak-mcp-daemon.mjs index dd28709..bcaa3ec 100755 --- a/bin/iak-mcp-daemon.mjs +++ b/bin/iak-mcp-daemon.mjs @@ -84,8 +84,15 @@ 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. + const pollerIntervalMs = Math.max(1000, Number(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 +100,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 From e9b405d63bcacfe81891f694278cf12a6bed269a Mon Sep 17 00:00:00 2001 From: claudemm Date: Sun, 13 Sep 2026 00:23:46 +0300 Subject: [PATCH 2/2] perf: let the approval gate be tuned apart from the room read Review from @claudeMB on #101: this poller does two jobs that want opposite things. Reading the room is paid per request; carrying petrus's /approve tap is the one interval a human feels, and 30 seconds after tapping Approve reads as broken rather than thrifty. Coupling them to one number forces a box to pick. mcp.confirmations.interval_sec now takes precedence, falling back to poller.interval_sec and then the old 5s default, so no latency moves unless someone sets a key. Also documents the intervalMs default in src/confirmations.mjs (@grok): any caller that omits it inherits 5s, which is 17,280 requests/day/device. Tests: 21/21 pass. Co-Authored-By: Claude Opus 5 --- bin/iak-mcp-daemon.mjs | 11 ++++++++++- src/confirmations.mjs | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/bin/iak-mcp-daemon.mjs b/bin/iak-mcp-daemon.mjs index bcaa3ec..c9552b1 100755 --- a/bin/iak-mcp-daemon.mjs +++ b/bin/iak-mcp-daemon.mjs @@ -90,7 +90,16 @@ if (!apiKey) { // 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. - const pollerIntervalMs = Math.max(1000, Number(config?.poller?.interval_sec ?? 5) * 1000); + // 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: pollerIntervalMs, // Exact owner identities allowed to settle intents (config, with the 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');