@slack/radar-mcp 1.7.0 → 1.9.0
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/README.md +14 -5
- package/dist/mcp/index.js +134 -15
- package/dist/mcp/radar-response.d.ts +21 -0
- package/dist/mcp/radar-response.js +38 -0
- package/dist/mcp/session-backfill.d.ts +26 -0
- package/dist/mcp/session-backfill.js +156 -0
- package/dist/mcp/session-detail.d.ts +36 -0
- package/dist/mcp/session-detail.js +109 -0
- package/dist/mcp/session-query.d.ts +57 -0
- package/dist/mcp/session-query.js +327 -0
- package/dist/mcp/snapshot.d.ts +4 -2
- package/dist/mcp/snapshot.js +25 -12
- package/dist/mcp/tools.js +25 -3
- package/dist/shared/constants.d.ts +8 -0
- package/dist/shared/constants.js +8 -0
- package/dist/shared/db.d.ts +13 -0
- package/dist/shared/db.js +30 -3
- package/dist/shared/event-spool.d.ts +75 -0
- package/dist/shared/event-spool.js +366 -0
- package/dist/shared/stream.d.ts +5 -1
- package/dist/shared/stream.js +39 -0
- package/dist/web/log-session.js +6 -0
- package/dist/web/public/next/store.js +54 -2
- package/package.json +3 -2
|
@@ -100,6 +100,12 @@ class Store {
|
|
|
100
100
|
_localPushKey = null;
|
|
101
101
|
_lastBoundLabel = null;
|
|
102
102
|
_backfillToken = 0;
|
|
103
|
+
// True while the connection is in a dropped/waiting state. Lets pollHealth detect the
|
|
104
|
+
// drop->recover EDGE (device came back) so it can re-open the stream + backfill ONCE on
|
|
105
|
+
// recovery, instead of every poll. Without this, an auto-recovered tab flips the dot to
|
|
106
|
+
// connected but the dead EventSource is never re-opened and the device ring is never pulled,
|
|
107
|
+
// so the tab shows nothing until a manual refresh.
|
|
108
|
+
_dropped = false;
|
|
103
109
|
_rafPending = false;
|
|
104
110
|
_intervals = [];
|
|
105
111
|
// ---- shared detail cache (bounded FIFO) ----
|
|
@@ -204,6 +210,7 @@ class Store {
|
|
|
204
210
|
this._localPushKey = null;
|
|
205
211
|
this._lastBoundLabel = null;
|
|
206
212
|
this._backfillToken = 0;
|
|
213
|
+
this._dropped = false;
|
|
207
214
|
this._rafPending = false;
|
|
208
215
|
}
|
|
209
216
|
// ---- start everything ----
|
|
@@ -317,7 +324,12 @@ class Store {
|
|
|
317
324
|
this.closeStream();
|
|
318
325
|
const es = new EventSource("/stream");
|
|
319
326
|
es.onmessage = (m) => this.onFrame(m);
|
|
320
|
-
|
|
327
|
+
// An SSE error is the most direct signal the stream died; mark dropped so the next healthy
|
|
328
|
+
// poll re-opens + backfills (recovered()), not just flips the dot green.
|
|
329
|
+
es.onerror = () => {
|
|
330
|
+
this._dropped = true;
|
|
331
|
+
this.setConn("waiting", "reconnecting…");
|
|
332
|
+
};
|
|
321
333
|
this._es = es;
|
|
322
334
|
}
|
|
323
335
|
closeStream() {
|
|
@@ -349,6 +361,10 @@ class Store {
|
|
|
349
361
|
if (!STREAM_KINDS.includes(f.type))
|
|
350
362
|
return;
|
|
351
363
|
this._lastEventTs = Date.now();
|
|
364
|
+
// A live frame proves the stream healed itself (the browser EventSource auto-reconnects on a
|
|
365
|
+
// transient blip). Clear _dropped here so recovered() does NOT later tear down this healthy
|
|
366
|
+
// stream and re-open it: recovered() should only fire when the stream did NOT self-heal.
|
|
367
|
+
this._dropped = false;
|
|
352
368
|
this.setConn("connected", "");
|
|
353
369
|
const ev = f.event || {};
|
|
354
370
|
ev._type = f.type;
|
|
@@ -593,6 +609,7 @@ class Store {
|
|
|
593
609
|
async pollHealth() {
|
|
594
610
|
if (Date.now() - this._lastEventTs < 6000) {
|
|
595
611
|
this._healthMisses = 0;
|
|
612
|
+
this.recovered();
|
|
596
613
|
this.setConn("connected", "");
|
|
597
614
|
return;
|
|
598
615
|
}
|
|
@@ -600,17 +617,31 @@ class Store {
|
|
|
600
617
|
const h = await (await fetch("/health")).json();
|
|
601
618
|
if (h.device) {
|
|
602
619
|
this._healthMisses = 0;
|
|
620
|
+
this.recovered();
|
|
603
621
|
this.setConn("connected", "");
|
|
604
622
|
}
|
|
605
623
|
else if (++this._healthMisses >= 2) {
|
|
624
|
+
this._dropped = true;
|
|
606
625
|
this.setConn("waiting", "device offline, retrying…");
|
|
607
626
|
}
|
|
608
627
|
}
|
|
609
628
|
catch {
|
|
610
|
-
if (++this._healthMisses >= 2)
|
|
629
|
+
if (++this._healthMisses >= 2) {
|
|
630
|
+
this._dropped = true;
|
|
611
631
|
this.setConn("waiting", "reconnecting…");
|
|
632
|
+
}
|
|
612
633
|
}
|
|
613
634
|
}
|
|
635
|
+
// Called when health flips back to connected. If we were in a dropped state, the EventSource
|
|
636
|
+
// did not survive the drop and the tab has been silent, so re-open the stream + backfill the
|
|
637
|
+
// device ring ONCE on the recovery edge. The _dropped guard makes this fire on the transition,
|
|
638
|
+
// not on every healthy poll (resyncStream clears the flag). The <6s-since-last-event fast path
|
|
639
|
+
// means a tab that never lost its stream skips this (live frames keep _lastEventTs fresh).
|
|
640
|
+
recovered() {
|
|
641
|
+
if (!this._dropped)
|
|
642
|
+
return;
|
|
643
|
+
this.resyncStream();
|
|
644
|
+
}
|
|
614
645
|
async pollProfile() {
|
|
615
646
|
try {
|
|
616
647
|
const [profilesR, healthR] = await Promise.all([
|
|
@@ -635,6 +666,26 @@ class Store {
|
|
|
635
666
|
/* leave the last label */
|
|
636
667
|
}
|
|
637
668
|
}
|
|
669
|
+
// Re-open the SSE stream and backfill the device ring for the ACTIVE streaming source, so the
|
|
670
|
+
// current tab repopulates from what the device already holds (not just new live events).
|
|
671
|
+
// Needed because the EventSource does not survive a device drop and pollSpec only re-applies
|
|
672
|
+
// (and so re-opens+backfills) on a spec-KEY change, which a reconnect is not. Guarded for
|
|
673
|
+
// streaming sources only, matching applySpec: db + blank own no SSE feed; log opens the stream
|
|
674
|
+
// but backfill() self-skips it (host-side capture, no device ring to replay).
|
|
675
|
+
//
|
|
676
|
+
// Clears _dropped itself so the two recovery entry points -- the manual reconnect() button and
|
|
677
|
+
// the auto recovered() edge -- collapse into one idempotent op: whichever fires first wins, the
|
|
678
|
+
// other sees _dropped false and no-ops. Without this, a manual reconnect on a waiting tab fires
|
|
679
|
+
// resyncStream() here AND again from recovered() once the health poll resolves, so the stream is
|
|
680
|
+
// opened, torn down, reopened, and the ring is backfilled twice.
|
|
681
|
+
resyncStream() {
|
|
682
|
+
this._dropped = false;
|
|
683
|
+
const src = this.spec.source;
|
|
684
|
+
if (this.spec.blank || !src || src === "db")
|
|
685
|
+
return;
|
|
686
|
+
this.openStream();
|
|
687
|
+
this.backfill();
|
|
688
|
+
}
|
|
638
689
|
async reconnect() {
|
|
639
690
|
try {
|
|
640
691
|
await fetch("/enable", { method: "POST" });
|
|
@@ -645,6 +696,7 @@ class Store {
|
|
|
645
696
|
this.pollHealth();
|
|
646
697
|
this.pollProfile();
|
|
647
698
|
this.pollSpec();
|
|
699
|
+
this.resyncStream();
|
|
648
700
|
}
|
|
649
701
|
}
|
|
650
702
|
// ---- aggregate math (moved here from widgets per the plan: it belongs WITH the aggregate
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slack/radar-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "MCP server and web dashboard for on-device debugging of the Slack Android app via ADB",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/mcp/index.js",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"start": "node dist/mcp/index.js",
|
|
23
23
|
"start:web": "node dist/web/bin.js",
|
|
24
24
|
"watch": "tsc --watch",
|
|
25
|
-
"test": "npm run build && node
|
|
25
|
+
"test": "npm run build && node tests/run.mjs",
|
|
26
|
+
"test:tap": "npm run build && node --test --test-force-exit tests/*.test.mjs",
|
|
26
27
|
"test:smoke": "npm run build && bash tests/smoke.sh",
|
|
27
28
|
"test:all": "npm run test && npm run test:smoke",
|
|
28
29
|
"prepare": "npm run build"
|