slashvibe-mcp 0.8.23 → 0.8.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -658,6 +658,7 @@ class VibeMCPServer {
658
658
  // The HTTP store creates one random key per logical send and preserves it
659
659
  // across its internal transport retries. JSON-RPC ids restart every MCP
660
660
  // process, so deriving a durable key from `id` would collapse future sends.
661
+ presence.noteActivity(); // a tool call = the person is here; polls snap back to fast
661
662
  const result = await tool.handler(args);
662
663
 
663
664
  // Emit list_changed notification for state-changing tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slashvibe-mcp",
3
- "version": "0.8.23",
3
+ "version": "0.8.24",
4
4
  "mcpName": "io.github.vibecodinginc/vibe",
5
5
  "description": "Presence + messaging for terminal coding agents (Claude Code, Codex, Cursor) — the /vibe kernel",
6
6
  "main": "index.js",
package/presence.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * Uses session tokens for per-session identity.
6
6
  * Users become "idle" after 5 minutes of no heartbeat.
7
7
  *
8
- * Also runs a guest message poll every 15 seconds. When guest messages
8
+ * Also polls for DMs and guest messages on an idle-aware cadence. When they
9
9
  * are detected, emits a tools/list_changed notification to force Claude
10
10
  * to re-query vibe tools (which triggers getPresenceFooter and injects
11
11
  * the messages into context). This ensures Use Case 2 (user testing/QA)
@@ -18,9 +18,54 @@ const store = require('./store');
18
18
  const { apiHeaders } = require('./api-auth');
19
19
 
20
20
  let heartbeatInterval = null;
21
- let guestPollInterval = null;
21
+ let unreadPollTimer = null;
22
+ let guestPollTimer = null;
22
23
  let sessionInitialized = false;
23
24
 
25
+ // ---- Poll cadence (2026-09-02 audit) -------------------------------------
26
+ // One idle signed-in client made 2 inbox reads every 15s forever — session/guest
27
+ // (6 KV commands) + the full threads list (1) — and the fleet's long-lived
28
+ // sessions (9-hour and 9-day-old windows) added up to ~7 inbox polls/sec at
29
+ // the KV. Replies must still land fast, so the DM check stays at 15s while the
30
+ // person is ACTIVE (any tool call in the last ACTIVE_WINDOW_MS) and backs off
31
+ // to 60s when idle; the guest-session poll, a pairing feature almost nobody is
32
+ // in, runs at 60s active / 300s idle. Any tool call snaps both back to fast.
33
+ // Worst-case DM latency: 15s active, 60s idle.
34
+ const ACTIVE_WINDOW_MS = Number(process.env.VIBE_POLL_ACTIVE_WINDOW_MS) || 10 * 60 * 1000;
35
+ const CADENCE = {
36
+ active: { unread: 15 * 1000, guest: 60 * 1000 },
37
+ idle: { unread: 60 * 1000, guest: 300 * 1000 },
38
+ };
39
+ let lastActivityAt = Date.now(); // the session starting is activity
40
+
41
+ function noteActivity() {
42
+ const wasIdle = cadenceFor(Date.now() - lastActivityAt) === CADENCE.idle;
43
+ lastActivityAt = Date.now();
44
+ // A pending idle timer would still fire on the slow cadence (codex P2 on #37):
45
+ // re-arm both polls on the fast cadence the moment the person is back.
46
+ if (wasIdle) {
47
+ if (unreadPollTimer) { clearTimeout(unreadPollTimer); schedule('unread'); }
48
+ if (guestPollTimer) { clearTimeout(guestPollTimer); schedule('guest'); }
49
+ }
50
+ }
51
+
52
+ /** Pure: which cadence applies after `idleMs` without a tool call. */
53
+ function cadenceFor(idleMs) {
54
+ return idleMs < ACTIVE_WINDOW_MS ? CADENCE.active : CADENCE.idle;
55
+ }
56
+
57
+ function schedule(kind) {
58
+ const ms = cadenceFor(Date.now() - lastActivityAt)[kind];
59
+ const run = async () => {
60
+ if (kind === 'unread') await pollUnread(); else await pollGuestMessages();
61
+ // Re-arm only while start() is in effect (stop() clears the timers).
62
+ if (kind === 'unread' ? unreadPollTimer : guestPollTimer) schedule(kind);
63
+ };
64
+ const t = setTimeout(run, ms);
65
+ t.unref?.();
66
+ if (kind === 'unread') unreadPollTimer = t; else guestPollTimer = t;
67
+ }
68
+
24
69
  function start() {
25
70
  if (heartbeatInterval) return;
26
71
 
@@ -30,11 +75,9 @@ function start() {
30
75
  // Then every 30 seconds
31
76
  heartbeatInterval = setInterval(sendHeartbeat, 30 * 1000);
32
77
 
33
- // Guest message poll: every 15 seconds, check for incoming session messages
34
- // and emit tools/list_changed to force Claude to pick them up via getPresenceFooter
35
- if (!guestPollInterval) {
36
- guestPollInterval = setInterval(pollGuestMessages, 15 * 1000);
37
- }
78
+ // Inbox polls on the idle-aware cadence above (see CADENCE).
79
+ if (!unreadPollTimer) schedule('unread');
80
+ if (!guestPollTimer) schedule('guest');
38
81
  }
39
82
 
40
83
  function stop() {
@@ -42,9 +85,13 @@ function stop() {
42
85
  clearInterval(heartbeatInterval);
43
86
  heartbeatInterval = null;
44
87
  }
45
- if (guestPollInterval) {
46
- clearInterval(guestPollInterval);
47
- guestPollInterval = null;
88
+ if (unreadPollTimer) {
89
+ clearTimeout(unreadPollTimer);
90
+ unreadPollTimer = null;
91
+ }
92
+ if (guestPollTimer) {
93
+ clearTimeout(guestPollTimer);
94
+ guestPollTimer = null;
48
95
  }
49
96
  // Clean up session file
50
97
  config.clearSession();
@@ -97,13 +144,11 @@ async function pollGuestMessages() {
97
144
  try {
98
145
  const handle = config.getHandle();
99
146
  if (!handle) return;
100
-
101
147
  const apiUrl = config.getApiUrl?.() || 'https://www.slashvibe.dev';
102
148
  const resp = await fetch(`${apiUrl}/api/session/guest?handle=${encodeURIComponent(handle)}`, {
103
149
  headers: apiHeaders(),
104
150
  });
105
151
  if (!resp.ok) return;
106
-
107
152
  const data = await resp.json();
108
153
  if (data.success && Array.isArray(data.messages) && data.messages.length > 0) {
109
154
  // Guest messages waiting — emit list_changed to force Claude to refresh
@@ -112,11 +157,21 @@ async function pollGuestMessages() {
112
157
  global.vibeNotifier.emitImmediate();
113
158
  }
114
159
  }
160
+ } catch {
161
+ // Silent fail — guest polling is best-effort
162
+ }
163
+ }
115
164
 
116
- // Ordinary DMs: nudge the session when the unread count RISES, so a DM
117
- // sent from any surface reaches the live session within one poll cycle
118
- // (getPresenceFooter injects the new bodies, once each). A drop just
119
- // rebaselines — reading elsewhere shouldn't trigger a refresh here.
165
+ /**
166
+ * Ordinary DMs: nudge the session when the unread count RISES, so a DM sent
167
+ * from any surface reaches the live session within one poll cycle
168
+ * (getPresenceFooter injects the new bodies, once each). A drop just
169
+ * rebaselines — reading elsewhere shouldn't trigger a refresh here.
170
+ */
171
+ async function pollUnread() {
172
+ try {
173
+ const handle = config.getHandle();
174
+ if (!handle) return;
120
175
  const unread = await store.getUnreadCount(handle).catch(() => null);
121
176
  if (typeof unread === 'number') {
122
177
  if (lastSeenUnread !== null && unread > lastSeenUnread && global.vibeNotifier) {
@@ -125,7 +180,7 @@ async function pollGuestMessages() {
125
180
  lastSeenUnread = unread;
126
181
  }
127
182
  } catch {
128
- // Silent fail — guest polling is best-effort
183
+ // Silent fail — best-effort
129
184
  }
130
185
  }
131
186
 
@@ -170,4 +225,8 @@ async function goOffline() {
170
225
  }
171
226
  }
172
227
 
173
- module.exports = { start, stop, forceHeartbeat, goOffline };
228
+ module.exports = { start, stop, forceHeartbeat, goOffline, noteActivity, cadenceFor, CADENCE, ACTIVE_WINDOW_MS,
229
+ // test seams — read the armed delays, and age the session without waiting
230
+ _pollDelaysForTest: () => ({ unread: unreadPollTimer?._idleTimeout ?? null, guest: guestPollTimer?._idleTimeout ?? null }),
231
+ _setLastActivityForTest: (ts) => { lastActivityAt = ts; },
232
+ };
package/version.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
- "version": "0.8.23",
3
- "updated": "2026-09-01",
4
- "changelog": "The install tells the truth about the room. Setup used to say “0 builders online” to every newcomer, because the roster is private before you sign in and it was counting names it was never given; now it reads the count the server actually serves, and after sign-in it lists real people and what they are working on. And a first message to someone before you have signed in no longer asks GitHub to sign you in as them.",
2
+ "version": "0.8.24",
3
+ "updated": "2026-09-02",
4
+ "changelog": "Quieter when you are away. The server used to check your inbox twice every fifteen seconds for as long as your coding session stayed open — days, sometimes. Now it checks every fifteen seconds while you are actually using /vibe and backs off to once a minute when you are not; the moment you use a tool it is fast again. Replies still reach a live session within fifteen seconds, and within a minute of an idle one.",
5
5
  "features": [
6
- "Setup says how many people are here now — never a zero it did not measure",
7
- "After sign-in, setup lists the people actually here and what they are working on",
8
- "Messaging @someone before sign-in starts your own sign-in, not one for them",
9
- "Setup's quick start is a sentence you can say, not a command to learn"
6
+ "Inbox checks back off to once a minute when a session is idle, and snap back the moment you use a tool",
7
+ "The rarely used guest-session poll runs once a minute while active and every five minutes while idle",
8
+ "Presence heartbeat unchanged: you stay visible exactly as before"
10
9
  ],
11
10
  "deprecated": [],
12
11
  "breaking": false,