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 +1 -0
- package/package.json +1 -1
- package/presence.js +77 -18
- package/version.json +6 -7
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
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
|
|
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
|
|
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
|
-
//
|
|
34
|
-
|
|
35
|
-
if (!
|
|
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 (
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 —
|
|
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.
|
|
3
|
-
"updated": "2026-09-
|
|
4
|
-
"changelog": "
|
|
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
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
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,
|