@thehammer/danx-dashboard-mcp 0.1.287 → 0.1.289
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 +1 -1
- package/dist/index.js +7 -5
- package/dist/json-body.js +3 -14
- package/dist/listen.js +6 -32
- package/dist/plan-events-relay.js +40 -15
- package/dist/session-stream.js +2 -25
- package/dist/stop-fixes.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Card and plan reads/writes go through `danxbot_api` against the issue/plan route
|
|
|
63
63
|
|
|
64
64
|
A program calls this, not a person: the danxbot Claude Code plugin's relay loop, inside the session, calls it over and over on the session's own danx-dashboard MCP server and hands every event it gets to the session. A plan-orchestrator dispatch (a headless session that works one plan) has no plugin relay, so it keeps ONE background waiter sub-agent that loops on this tool until events exist and returns them with the last cursor, and the orchestrator passes that cursor to the next waiter it arms (DX-4667); the `plan-orchestrator` profile alone is granted the tool on a dispatch (it is an opt-in tool). The `bridge` subcommand is gone: no process is spawned, no `npx` cold start, and nothing outlives the session, because the stream lives in the MCP server, which ends with its session.
|
|
65
65
|
|
|
66
|
-
Input `{plan_id: number, cursor: string | null, timeout_ms: 1000..30000, transcript_path?: string}`. Answer, one of:
|
|
66
|
+
Input `{plan_id: number | null, cursor: string | null, timeout_ms: 1000..30000, transcript_path?: string}`. `plan_id` is `null` for a signed-in session on no plan (DX-4882): its stream still carries the pacing and resume messages of its Claude account, and it receives no plan events. A move between a plan and `null` starts clean like a move between plans: the plan-scoped records of the plan left are dropped, the session-scoped ones kept. Answer, one of:
|
|
67
67
|
|
|
68
68
|
- `{events: [{cursor: string, text: string, urgent: boolean}]}`: every record the caller has not delivered, in arrival order (empty at the timeout). Each record carries the cursor to store once THAT record is delivered. There is no `id`: the caller compares nothing. `urgent` (DX-4721) is decided in one place by the record's kind (`src/urgent-records.ts`): true for a critical usage-pacing stop and the revoked-key notice, false for everything else (activity events, nudge, resume message, digest, the relay's failure notice, an unreadable line). The caller adds an urgent record to a running turn at once and delivers any other after the turn ends; `urgent` never changes order, seq or acknowledgement. The dashboard writes no operator-stop record yet; when it does, it becomes one more kind in that function.
|
|
69
69
|
- `{stopped: {reason, detail, fix}}`: a terminal outcome (`not_connected`, `plan_archived`, `unauthorized`, `key_revoked`, `mint_refused`, `mint_bad_response`, `session_is_worker`, `superseded`, `replaced`, `revoked`, `refused`, `credential_unavailable`, `relay_failed`). `fix` is the remedy text (`STOP_FIXES`, `src/stop-fixes.ts`). The next call starts from scratch.
|
package/dist/index.js
CHANGED
|
@@ -768,7 +768,7 @@ async (args) => {
|
|
|
768
768
|
await syncToolsAfterSignIn();
|
|
769
769
|
// DX-3997 — a (re)connect starts or re-keys the plugin's relay loop; the next `danxbot_api` must re-read health.
|
|
770
770
|
listenerReminder.invalidate();
|
|
771
|
-
// DX-2985 — a leave drops the local connection record (the relay's
|
|
771
|
+
// DX-2985 — a leave drops the local connection record (its stream stays open, DX-4882: the relay keeps serving the session's pacing and resume messages; a later connect writes a fresh one).
|
|
772
772
|
if (args.disconnect === true) {
|
|
773
773
|
removeSessionConnectionAfterLeave(result, config.session?.id);
|
|
774
774
|
return jsonResult(result);
|
|
@@ -789,20 +789,22 @@ async (args) => {
|
|
|
789
789
|
});
|
|
790
790
|
// ---------------- plan_events_wait (DX-4233) ----------------
|
|
791
791
|
strictTool("plan_events_wait",
|
|
792
|
+
// DX-4882 - `plan_id` is null for a signed-in session on no plan: its stream still carries its account's pacing and resume messages.
|
|
792
793
|
// DX-4233 - for the danxbot plugin's relay loop. DX-4667 - and for a background waiter sub-agent of a plan orchestrator, which
|
|
793
794
|
// loops on it until events exist and returns them (and the cursor) to the orchestrator it wakes. It answers `events` (each
|
|
794
795
|
// `{cursor, text, urgent}`; empty after `timeout_ms`), the cursor to send being the last delivered event's, or `stopped` with the remedy
|
|
795
796
|
// to tell the session. General-audience text (DX-4550): nothing here names our own setup.
|
|
796
|
-
"Waits for events
|
|
797
|
-
"and hands every event to the session; do not call it for any other reason.
|
|
798
|
-
"
|
|
797
|
+
"Waits for events for this session: the events of the plan it is connected to, and the messages about its usage. For a relay " +
|
|
798
|
+
"or a background waiter that calls it in a loop and hands every event to the session; do not call it for any other reason. " +
|
|
799
|
+
"`plan_id` is the plan the session is on, or null when it is on no plan (then only the usage messages arrive; a different " +
|
|
800
|
+
"plan than the last call's, or a move to or from null, starts clean). `cursor` is the `cursor` of the last event delivered, as answered (null " +
|
|
799
801
|
"when none). Answers `{events: [{cursor, text, urgent}]}`, with `events` empty once `timeout_ms` passes, or `{stopped: " +
|
|
800
802
|
"{reason, detail, fix}}` when the stream ended: tell the session the `fix`. An event stays until a later call's " +
|
|
801
803
|
"`cursor` passes it. `urgent` is true for an event the session must hear at once (a critical usage limit, a revoked " +
|
|
802
804
|
"key) and false for every other: deliver an urgent one to the session even while it is working, and any other once its " +
|
|
803
805
|
"current turn ends. Only one wait is pending per session: a newer call ends the older. " +
|
|
804
806
|
"`transcript_path` is the session's transcript file, for its usage and title.", {
|
|
805
|
-
plan_id: z.number().int().positive().describe("Id of the plan this session is connected to."),
|
|
807
|
+
plan_id: z.number().int().positive().nullable().describe("Id of the plan this session is connected to, or null when it is on no plan."),
|
|
806
808
|
cursor: z
|
|
807
809
|
.string()
|
|
808
810
|
.nullable()
|
package/dist/json-body.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DX-2920 — the ONE place that reads a JSON body's top-level `.error` string.
|
|
3
3
|
*
|
|
4
|
-
* WHY THIS EXISTS. the stream client (`session-stream.ts`, classifying a mint refusal
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* different way: the mint code parsed the body directly, `listen.ts` never
|
|
8
|
-
* looked past a flattened, human-readable detail string at all — a caller
|
|
9
|
-
* that wanted to react to a SPECIFIC refusal (the admission-time 409 the
|
|
10
|
-
* mint route also uses) had to regex-scrape that flattened string back apart,
|
|
11
|
-
* a second, fragile source of truth for the same fact. This module is generic
|
|
12
|
-
* — it knows nothing about "session_not_connected" or "stream_ticket_invalid",
|
|
13
|
-
* only that a JSON object may carry a top-level string field named `error` —
|
|
14
|
-
* so it can live in BOTH the domain-aware `session-stream.ts` and the deliberately
|
|
15
|
-
* generic `listen.ts` (whose own docblock states it carries no domain
|
|
16
|
-
* knowledge of the dashboard's specific error shapes) without either one
|
|
17
|
-
* importing the other.
|
|
4
|
+
* WHY THIS EXISTS. the stream client (`session-stream.ts`, classifying a mint refusal such as `409 session_is_worker`) reads a refusal's
|
|
5
|
+
* error code from the parsed body, and this is the one place that decides what "carries an error code" means. Generic: it knows no
|
|
6
|
+
* dashboard error name, only that a JSON object may carry a top-level string field named `error`.
|
|
18
7
|
*/
|
|
19
8
|
export function errorCodeOf(body) {
|
|
20
9
|
return typeof body === "object" && body !== null && typeof body.error === "string"
|
package/dist/listen.js
CHANGED
|
@@ -16,16 +16,10 @@
|
|
|
16
16
|
* DX-4342's `resume` frame (`formatResumeLine`): all are advisory, written fresh by the dashboard's own keep-alive tick,
|
|
17
17
|
* and never replayed, so they ride this same shape with `id: null` rather
|
|
18
18
|
* than growing a THIRD output variant.
|
|
19
|
-
* - `{type:"stopped", reason, detail
|
|
19
|
+
* - `{type:"stopped", reason, detail}` — once, last. `reason`
|
|
20
20
|
* is the dashboard's own end reason (`superseded`, `replaced`, `revoked`,
|
|
21
21
|
* `not_connected`), `refused` (the ticket was not admitted), or
|
|
22
|
-
* `lease_expired` (no healthy connection for the lease).
|
|
23
|
-
* `refusal` is populated ONLY for `reason === "refused"`: `{status,
|
|
24
|
-
* errorCode, body}`, the admission response parsed ONCE here, so a
|
|
25
|
-
* consumer can recognize a SPECIFIC refusal (the dashboard's own
|
|
26
|
-
* `409 session_not_connected`, the same shape the mint route uses) by
|
|
27
|
-
* status + error code rather than re-deriving it from the flattened
|
|
28
|
-
* `detail` string. A wire `reason` value this build does not recognize (a
|
|
22
|
+
* `lease_expired` (no healthy connection for the lease). A wire `reason` value this build does not recognize (a
|
|
29
23
|
* newer dashboard's reason an older client hasn't shipped yet)
|
|
30
24
|
* normalizes to `"unknown"` rather than passing the raw string through,
|
|
31
25
|
* so the closed set stays closed — `fixForStop`'s generic
|
|
@@ -48,7 +42,6 @@
|
|
|
48
42
|
* (`src/issues/db/issue-activity.ts`); this published package cannot import
|
|
49
43
|
* danxbot source.
|
|
50
44
|
*/
|
|
51
|
-
import { errorCodeOf } from "./json-body.js";
|
|
52
45
|
import { LINE_PREFIX, oneLine } from "./one-line.js";
|
|
53
46
|
import { pacingRelayLine } from "./pacing-relay.js";
|
|
54
47
|
import { isUrgent } from "./urgent-records.js";
|
|
@@ -490,8 +483,8 @@ export async function runListener(options, deps) {
|
|
|
490
483
|
};
|
|
491
484
|
for (const id of capResumeIds(options.resumeIds))
|
|
492
485
|
remember(id);
|
|
493
|
-
function stop(reason, detail, code
|
|
494
|
-
deps.write({ type: "stopped", reason, detail
|
|
486
|
+
function stop(reason, detail, code) {
|
|
487
|
+
deps.write({ type: "stopped", reason, detail });
|
|
495
488
|
return code;
|
|
496
489
|
}
|
|
497
490
|
/**
|
|
@@ -614,24 +607,7 @@ export async function runListener(options, deps) {
|
|
|
614
607
|
const response = await deps.fetch(options.streamUrl, { headers, signal: controller.signal });
|
|
615
608
|
if (response.status >= 400 && response.status < 500 && !RETRYABLE_CLIENT_STATUSES.has(response.status)) {
|
|
616
609
|
const text = await response.text();
|
|
617
|
-
|
|
618
|
-
// classify by status + error code (the relay does, matching a specific
|
|
619
|
-
// admission refusal such as the dashboard's own `409 session_not_connected`)
|
|
620
|
-
// rather than re-deriving it from the flattened `detail` string below.
|
|
621
|
-
let body = null;
|
|
622
|
-
try {
|
|
623
|
-
body = text === "" ? null : JSON.parse(text);
|
|
624
|
-
}
|
|
625
|
-
catch {
|
|
626
|
-
body = null;
|
|
627
|
-
}
|
|
628
|
-
return {
|
|
629
|
-
kind: "refused",
|
|
630
|
-
status: response.status,
|
|
631
|
-
errorCode: errorCodeOf(body),
|
|
632
|
-
body,
|
|
633
|
-
detail: `HTTP ${response.status} ${oneLine(text, 300)}`,
|
|
634
|
-
};
|
|
610
|
+
return { kind: "refused", detail: `HTTP ${response.status} ${oneLine(text, 300)}` };
|
|
635
611
|
}
|
|
636
612
|
if (!response.ok || response.body === null) {
|
|
637
613
|
await response.body?.cancel();
|
|
@@ -682,9 +658,7 @@ export async function runListener(options, deps) {
|
|
|
682
658
|
return stop(outcome.reason, "the dashboard ended this listener", 1);
|
|
683
659
|
}
|
|
684
660
|
if (outcome.kind === "refused") {
|
|
685
|
-
return stop("refused", `the dashboard refused the listener ticket: ${outcome.detail}`, 1
|
|
686
|
-
refusal: { status: outcome.status, errorCode: outcome.errorCode, body: outcome.body },
|
|
687
|
-
});
|
|
661
|
+
return stop("refused", `the dashboard refused the listener ticket: ${outcome.detail}`, 1);
|
|
688
662
|
}
|
|
689
663
|
if (outcome.detail !== undefined)
|
|
690
664
|
lastDropDetail = outcome.detail;
|
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
* THE ACCEPTED GAP: an insert that commits more than that 30 s overlap after the event it follows is missed by a RESTART
|
|
31
31
|
* resume (the dashboard's own limit); a running relay never misses one. And a burst of more than `CURSOR_IDS_MAX` events
|
|
32
32
|
* inside the overlap can replay the oldest of them once (a duplicate, never a loss).
|
|
33
|
-
* 3. ONE PLAN. A call names the plan the session is on (`planId`)
|
|
34
|
-
*
|
|
33
|
+
* 3. ONE PLAN. A call names the plan the session is on (`planId`), or `null` for a signed-in session on no plan (DX-4882: its stream
|
|
34
|
+
* still carries the pacing and resume messages of its account). A plan different from the last call's, to or from `null` included,
|
|
35
|
+
* drops the PLAN-scoped records buffered or held for the plan it left, so a move starts clean. SESSION-scoped records (pacing, resume
|
|
35
36
|
* and the relay's own failure and revoked-key notices: they say nothing about a plan, and the dashboard never replays
|
|
36
37
|
* them) are kept, here and across the 40 s suspend below. The ids of a dropped record leave the cursors of the records that
|
|
37
38
|
* stay: delivering a kept record must not claim an event that was dropped undelivered. The stream stays on a move: it
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
import { DigestHold } from "./digest-hold.js";
|
|
56
57
|
import { HeartbeatReporter } from "./heartbeat-reporter.js";
|
|
57
58
|
import { capResumeIds, DELIVERED_ID_MEMORY, HEALTHY_CONNECTION_MS, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, runListener, } from "./listen.js";
|
|
58
|
-
import {
|
|
59
|
+
import { fetchPlanInventory, HEARTBEAT_INTERVAL_MS, heartbeatConnection, MAX_REFUSED_TICKETS_IN_A_ROW, mintTicket, } from "./session-stream.js";
|
|
59
60
|
import { fixForStop } from "./stop-fixes.js";
|
|
60
61
|
import { isUrgent } from "./urgent-records.js";
|
|
61
62
|
/** The shortest and longest wait one call may ask for; a short ceiling keeps an orphaned waiter's life short. */
|
|
@@ -91,7 +92,8 @@ export class PlanEventRelay {
|
|
|
91
92
|
buffer = [];
|
|
92
93
|
waiter = null;
|
|
93
94
|
run = null;
|
|
94
|
-
|
|
95
|
+
/** The plan the last call named (`null` = a session on no plan); `undefined` before the first call, which has no plan to have moved from. */
|
|
96
|
+
lastPlanId = undefined;
|
|
95
97
|
/** The last seq given: it continues from a presented cursor and survives a suspend, so a cursor stays valid across the gap. */
|
|
96
98
|
seq = 0;
|
|
97
99
|
/** The dashboard event ids that arrived (or were presented in a cursor), oldest first, bounded: the next record's `recent`. */
|
|
@@ -103,6 +105,8 @@ export class PlanEventRelay {
|
|
|
103
105
|
connectionState = "reconnecting";
|
|
104
106
|
stoppedReason = null;
|
|
105
107
|
lastInventoryError = null;
|
|
108
|
+
/** DX-4882: whether the connected plan's inventory has been read in this run (a session on no plan has none to read). */
|
|
109
|
+
inventoryRead = false;
|
|
106
110
|
lastActivityAt = 0;
|
|
107
111
|
heartbeat = null;
|
|
108
112
|
reporter = null;
|
|
@@ -120,8 +124,10 @@ export class PlanEventRelay {
|
|
|
120
124
|
// DX-4233: one wait at a time; the older call answers empty so its caller is never left hanging.
|
|
121
125
|
this.supersedeWaiter();
|
|
122
126
|
// DX-4233: a plan move starts clean (the very first call has no last plan; after a suspend the last plan is still known, and `dropPlanScoped` already ran).
|
|
123
|
-
if (this.lastPlanId !==
|
|
127
|
+
if (this.lastPlanId !== undefined && args.planId !== this.lastPlanId) {
|
|
124
128
|
this.startClean();
|
|
129
|
+
this.planMovedInventory(args.planId);
|
|
130
|
+
}
|
|
125
131
|
this.lastPlanId = args.planId;
|
|
126
132
|
if (cursor !== null) {
|
|
127
133
|
// DX-4233: a fresh relay (a restarted server) continues the caller's sequence and remembers the ids it was handed; a
|
|
@@ -224,6 +230,31 @@ export class PlanEventRelay {
|
|
|
224
230
|
// dropped events belong to a plan the session left, and the dashboard replays only the session's current plan, so nothing
|
|
225
231
|
// legitimate is replayed into the guard; and the gap, which does need them replayed, ends the run, so `start` builds a fresh `seenIds` from the next call's cursor.
|
|
226
232
|
}
|
|
233
|
+
/** Whether the last call named a plan (not `null` = no plan, not `undefined` = no call yet). */
|
|
234
|
+
onAPlan() {
|
|
235
|
+
return typeof this.lastPlanId === "number";
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* DX-4882: the plan changed, so the inventory read for the old one no longer describes the session. On no plan there is nothing to read (and
|
|
239
|
+
* nothing to report as unavailable); on a plan, a live run reads it now (a run that is not live reads it when it starts).
|
|
240
|
+
*/
|
|
241
|
+
planMovedInventory(planId) {
|
|
242
|
+
this.inventoryRead = false;
|
|
243
|
+
this.lastInventoryError = null;
|
|
244
|
+
const run = this.run;
|
|
245
|
+
const auth = this.deps.auth();
|
|
246
|
+
if (planId === null || run === null || auth === null)
|
|
247
|
+
return;
|
|
248
|
+
void this.readInventory(run, auth, { ...this.deps, signal: run.abort.signal });
|
|
249
|
+
}
|
|
250
|
+
/** The inventory is a property of the credential and the plan: read best effort, never a gate; a failed read is reported on the heartbeat (`inventory: unavailable`) and tried again at the next re-mint. */
|
|
251
|
+
async readInventory(run, auth, http) {
|
|
252
|
+
const inventory = await fetchPlanInventory(auth, http);
|
|
253
|
+
if (!this.current(run))
|
|
254
|
+
return;
|
|
255
|
+
this.inventoryRead = inventory.kind === "ok";
|
|
256
|
+
this.lastInventoryError = inventory.kind === "ok" ? null : inventory.detail;
|
|
257
|
+
}
|
|
227
258
|
/** The session moved to another plan: drop what was buffered or held for the plan it left; session-scoped records stay. */
|
|
228
259
|
startClean() {
|
|
229
260
|
this.dropPlanScoped();
|
|
@@ -297,6 +328,7 @@ export class PlanEventRelay {
|
|
|
297
328
|
// replays its 30 s overlap before the highest of them, so a replayed one is dropped and a never-delivered one is delivered.
|
|
298
329
|
this.seenIds = capResumeIds([...(cursor?.ids ?? []), ...this.buffer.flatMap((r) => r.own)]);
|
|
299
330
|
this.stoppedReason = null;
|
|
331
|
+
this.inventoryRead = false;
|
|
300
332
|
const run = { abort: new AbortController(), forceReconnect: null };
|
|
301
333
|
this.run = run;
|
|
302
334
|
void this.loop(run).catch((err) => {
|
|
@@ -336,7 +368,6 @@ export class PlanEventRelay {
|
|
|
336
368
|
const http = { ...this.deps, signal: run.abort.signal };
|
|
337
369
|
let backoff = INITIAL_BACKOFF_MS;
|
|
338
370
|
let refusedInRow = 0;
|
|
339
|
-
let inventoryRead = false;
|
|
340
371
|
for (;;) {
|
|
341
372
|
if (!this.current(run))
|
|
342
373
|
return;
|
|
@@ -356,14 +387,11 @@ export class PlanEventRelay {
|
|
|
356
387
|
backoff = Math.min(MAX_BACKOFF_MS, backoff * 2);
|
|
357
388
|
continue;
|
|
358
389
|
}
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const inventory = await fetchPlanInventory(auth, http);
|
|
390
|
+
// DX-4882: only a session on a plan has an inventory to read.
|
|
391
|
+
if (!this.inventoryRead && this.onAPlan()) {
|
|
392
|
+
await this.readInventory(run, auth, http);
|
|
363
393
|
if (!this.current(run))
|
|
364
394
|
return;
|
|
365
|
-
inventoryRead = inventory.kind === "ok";
|
|
366
|
-
this.lastInventoryError = inventory.kind === "ok" ? null : inventory.detail;
|
|
367
395
|
}
|
|
368
396
|
backoff = INITIAL_BACKOFF_MS;
|
|
369
397
|
const attempt = { stopped: null, emitted: false };
|
|
@@ -412,9 +440,6 @@ export class PlanEventRelay {
|
|
|
412
440
|
continue;
|
|
413
441
|
}
|
|
414
442
|
if (stopped.reason === "refused") {
|
|
415
|
-
const classified = classifyAdmissionRefusal(stopped.refusal);
|
|
416
|
-
if (classified !== null)
|
|
417
|
-
return this.terminal(run, classified.reason, classified.detail);
|
|
418
443
|
const provedHealthy = attempt.emitted || this.deps.now() - startedAt >= HEALTHY_CONNECTION_MS;
|
|
419
444
|
refusedInRow = provedHealthy ? 1 : refusedInRow + 1;
|
|
420
445
|
if (refusedInRow >= MAX_REFUSED_TICKETS_IN_A_ROW) {
|
package/dist/session-stream.js
CHANGED
|
@@ -133,9 +133,8 @@ export async function mintTicket(options, deps) {
|
|
|
133
133
|
if (outcome.kind === "transient")
|
|
134
134
|
return outcome;
|
|
135
135
|
const { status, text, body } = outcome;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
136
|
+
// DX-4882: a session on no plan is minted a ticket like any other (its stream carries its account's pacing and resume messages), so
|
|
137
|
+
// `409 session_not_connected` is no longer an answer the mint gives.
|
|
139
138
|
// DX-3028 (AC2) — matched BEFORE the generic mint_refused fallback below,
|
|
140
139
|
// which every other 409 falls into: this session id belongs to a worker
|
|
141
140
|
// dispatch (`SessionIsWorkerError`, the same shape the heartbeat route
|
|
@@ -219,28 +218,6 @@ export async function fetchPlanInventory(options, deps) {
|
|
|
219
218
|
const b = body;
|
|
220
219
|
return { kind: "ok", boards: b.boards, cardCount: b.card_count };
|
|
221
220
|
}
|
|
222
|
-
/**
|
|
223
|
-
* DX-2920 — a stream admission refusal (`listen.ts`'s STRUCTURED `refused`
|
|
224
|
-
* outcome — `{status, errorCode, body}`, parsed once in `connectOnce`) whose
|
|
225
|
-
* shape matches one the mint route already gives its OWN name to: `409
|
|
226
|
-
* session_not_connected`, the SAME shape the mint route uses for "no plan
|
|
227
|
-
* connected" (`plan-session-stream.ts`, DX-2863 review finding M2). A session
|
|
228
|
-
* with no plan connected is the identical terminal state whether the
|
|
229
|
-
* dashboard says so at mint or at the stream's own admission check, and the
|
|
230
|
-
* session should see ONE remedy either way.
|
|
231
|
-
*
|
|
232
|
-
* Classifies by STATUS + ERROR CODE, never by re-deriving them from the
|
|
233
|
-
* flattened `detail` string — the second-source-of-truth shape this card's fix
|
|
234
|
-
* round closed (the regex-based `isSessionNotConnectedRefusal` it replaced).
|
|
235
|
-
*/
|
|
236
|
-
export function classifyAdmissionRefusal(refusal) {
|
|
237
|
-
if (refusal === undefined)
|
|
238
|
-
return null;
|
|
239
|
-
if (refusal.status === 409 && refusal.errorCode === "session_not_connected") {
|
|
240
|
-
return { reason: "not_connected", detail: "the session is not connected to a plan" };
|
|
241
|
-
}
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
221
|
/**
|
|
245
222
|
* The one real `scheduleInterval`: a `setInterval` and its canceler, for the relay's heartbeat. Unref'd: a heartbeat never keeps
|
|
246
223
|
* the MCP server alive after its session ended.
|
package/dist/stop-fixes.js
CHANGED
|
@@ -24,7 +24,7 @@ export const STOP_FIXES = {
|
|
|
24
24
|
unauthorized: "the dashboard no longer accepts this session's key (it lapsed after a day unused): call plan_connect again to request access",
|
|
25
25
|
key_revoked: "stop all work now, commit your work in progress and end this session: the person who owns this session's access revoked it; " +
|
|
26
26
|
"do not call plan_connect or request access again",
|
|
27
|
-
mint_refused: "check that this session is
|
|
27
|
+
mint_refused: "check that this session is signed in to the dashboard its MCP server points at, then call plan_connect again",
|
|
28
28
|
mint_bad_response: "check that DANXBOT_DASHBOARD_URL for this session's MCP server points at a danxbot dashboard",
|
|
29
29
|
// A newer ticket exists (superseded) or another connection is already using this one (replaced): THIS listener is ending
|
|
30
30
|
// because the session is already served elsewhere, not because anything is broken.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-dashboard-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.289",
|
|
4
4
|
"description": "Stdio MCP server fronting danxbot's dashboard API for agents (operator sessions and dispatched workers alike): the generic danxbot_api/danxbot_api_list/danxbot_api_spec tools reach any route the caller's credential may call, plus dedicated agent-specific tools (plan_connect, request_permission, attach_file, and danxbot_slack_reply) (DX-3465, PLN-11 R-25).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|