@thehammer/danx-dashboard-mcp 0.1.336 → 0.1.338
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/dist/index.js +2 -5
- package/dist/plan-events-relay.js +28 -15
- package/dist/shutdown.js +16 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -88,7 +88,7 @@ import { READ_IDLE_TIMEOUT_MS } from "./listen.js";
|
|
|
88
88
|
import { REQUEST_TIMEOUT_MS, realScheduleInterval, realSleep } from "./session-stream.js";
|
|
89
89
|
import { servePlanEventsWait } from "./plan-events-tool.js";
|
|
90
90
|
import { lookupRestartNotice, RESTART_NOTICE_ARGS } from "./restart-notice.js";
|
|
91
|
-
import { stopWhenSessionEnds } from "./shutdown.js";
|
|
91
|
+
import { makeSessionStop, stopWhenSessionEnds } from "./shutdown.js";
|
|
92
92
|
import { SUBAGENTS_LIVE_SUBCOMMAND, runSubagentsLiveCommand } from "./subagents-live.js";
|
|
93
93
|
import { resolveBootCredential } from "./credential.js";
|
|
94
94
|
import { SessionKeyUnusableError, deleteSessionKey } from "./session-key.js";
|
|
@@ -868,10 +868,7 @@ async function main() {
|
|
|
868
868
|
stdin: process.stdin,
|
|
869
869
|
signals: process,
|
|
870
870
|
exit: (code) => process.exit(code),
|
|
871
|
-
stop: ()
|
|
872
|
-
permissionWatch.stop();
|
|
873
|
-
return planEventRelay.close();
|
|
874
|
-
},
|
|
871
|
+
stop: makeSessionStop(permissionWatch, planEventRelay),
|
|
875
872
|
});
|
|
876
873
|
const transport = new StdioServerTransport();
|
|
877
874
|
await server.connect(transport);
|
|
@@ -115,7 +115,7 @@ export class PlanEventRelay {
|
|
|
115
115
|
lastActivityAt = 0;
|
|
116
116
|
heartbeat = null;
|
|
117
117
|
reporter = null;
|
|
118
|
-
/** DX-5415: the
|
|
118
|
+
/** DX-5415: the chain of every reporter stop still storing held finals (a suspend's and a transcript switch's); null when none is pending. Never rejects (see `chainSettling`). */
|
|
119
119
|
settling = null;
|
|
120
120
|
/** DX-5415: set by `close()` (the server's shutdown) and never cleared: a call paused across the close, or made after it, answers empty and starts nothing. */
|
|
121
121
|
closed = false;
|
|
@@ -154,10 +154,11 @@ export class PlanEventRelay {
|
|
|
154
154
|
this.buffer = this.buffer.map((r) => ({ ...r, recent: boundedIds([...cursor.ids, ...r.recent]) }));
|
|
155
155
|
}
|
|
156
156
|
this.touch();
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
this.ensureReporter(args.transcriptPath);
|
|
158
|
+
// DX-5415 / DX-4233: a transcript switch's settle rides `settling` (the chain a suspend uses), so every call paused on it resumes in call order and the older one cannot overwrite the newer call's waiter.
|
|
159
|
+
if (this.settling !== null) {
|
|
160
|
+
await this.settling;
|
|
161
|
+
// The pause let a close() or a cancel through; a close already let go of the stream, heartbeat and reporter, so none is started again.
|
|
161
162
|
if (signal?.aborted || this.closed)
|
|
162
163
|
return { events: [] };
|
|
163
164
|
}
|
|
@@ -175,6 +176,8 @@ export class PlanEventRelay {
|
|
|
175
176
|
this.start(cursor);
|
|
176
177
|
if (this.buffer.length > 0)
|
|
177
178
|
return this.answer();
|
|
179
|
+
// DX-5415 / DX-4233: supersede where the waiter is installed, not before the pauses: calls resume in call order, so the newest always holds the slot and an older one that resumed late cannot orphan the waiter beneath it.
|
|
180
|
+
this.supersedeWaiter();
|
|
178
181
|
return new Promise((resolve) => {
|
|
179
182
|
const onAbort = () => this.supersedeWaiter();
|
|
180
183
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -284,8 +287,12 @@ export class PlanEventRelay {
|
|
|
284
287
|
}
|
|
285
288
|
/** Creates the heartbeat reporter on the first call; a later call's transcript path (a resumed session) is handed to it. */
|
|
286
289
|
ensureReporter(transcriptPath) {
|
|
287
|
-
if (this.reporter !== null)
|
|
288
|
-
|
|
290
|
+
if (this.reporter !== null) {
|
|
291
|
+
const switching = this.reporter.useTranscript(transcriptPath);
|
|
292
|
+
if (switching !== null)
|
|
293
|
+
this.chainSettling(switching);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
289
296
|
this.reporter = new HeartbeatReporter({
|
|
290
297
|
auth: () => {
|
|
291
298
|
const auth = this.deps.auth();
|
|
@@ -302,7 +309,18 @@ export class PlanEventRelay {
|
|
|
302
309
|
announce: (text, source) => this.digest.route({ type: "event", id: null, text, origin: null, scope: "session", urgent: isUrgent({ source }) }),
|
|
303
310
|
forceReconnect: () => this.run?.forceReconnect?.(),
|
|
304
311
|
}, transcriptPath ?? null);
|
|
305
|
-
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* DX-5415: the one settle chain: the last settle and `stopping` both finish before it clears itself.
|
|
315
|
+
* `stopping` never rejects (a reporter's stop settles its own failures), so `settling` never rejects and
|
|
316
|
+
* every waiter on it resumes; do not add a swallowing fallback here.
|
|
317
|
+
*/
|
|
318
|
+
chainSettling(stopping) {
|
|
319
|
+
const settling = Promise.all([this.settling, stopping]).then(() => {
|
|
320
|
+
if (this.settling === settling)
|
|
321
|
+
this.settling = null;
|
|
322
|
+
});
|
|
323
|
+
this.settling = settling;
|
|
306
324
|
}
|
|
307
325
|
/** Mark the relay in use and make sure the heartbeat is scheduled. */
|
|
308
326
|
touch() {
|
|
@@ -346,13 +364,8 @@ export class PlanEventRelay {
|
|
|
346
364
|
this.run = null;
|
|
347
365
|
// DX-5415: the reporter's stop settles its collector's held sub-agent finals first; callers that can wait (shutdown, the next call) do.
|
|
348
366
|
const stopping = this.reporter?.stop() ?? null;
|
|
349
|
-
if (stopping !== null)
|
|
350
|
-
|
|
351
|
-
if (this.settling === settling)
|
|
352
|
-
this.settling = null;
|
|
353
|
-
});
|
|
354
|
-
this.settling = settling;
|
|
355
|
-
}
|
|
367
|
+
if (stopping !== null)
|
|
368
|
+
this.chainSettling(stopping);
|
|
356
369
|
this.reporter = null;
|
|
357
370
|
this.digest.reset();
|
|
358
371
|
this.dropPlanScoped();
|
package/dist/shutdown.js
CHANGED
|
@@ -8,14 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* DX-5415: `stop` returns the relay's bounded final settle (a sub-agent's held final report is stored before the collector stops).
|
|
10
10
|
* The first two triggers leave the process to end on its own once nothing keeps it alive, so the settle completes. A signal would
|
|
11
|
-
* kill it mid-settle, so its handler awaits `stop` and only then exits. `once
|
|
12
|
-
* default action,
|
|
11
|
+
* kill it mid-settle, so its handler awaits `stop` and only then exits. The handler is `once` per signal: a second signal of the
|
|
12
|
+
* SAME kind takes the default action, but a DIFFERENT one (SIGTERM then SIGINT) runs the handler again, which is safe because
|
|
13
|
+
* `stop` is idempotent. The settle is bounded (10 s), yet a parent that signals with an SDK client allows only about 4 s before
|
|
14
|
+
* it sends SIGKILL, so the held finals are stored best-effort within that window, not guaranteed.
|
|
13
15
|
*/
|
|
14
16
|
export function stopWhenSessionEnds(wiring) {
|
|
15
17
|
wiring.server.onclose = wiring.stop;
|
|
16
18
|
wiring.stdin.once("end", wiring.stop);
|
|
17
19
|
const exitAfterStop = () => {
|
|
18
|
-
|
|
20
|
+
// A synchronous throw from `stop` takes the same "shutdown failed" path as a rejection.
|
|
21
|
+
void Promise.resolve().then(wiring.stop).then(() => wiring.exit(0), (err) => {
|
|
19
22
|
process.stderr.write(`danx-dashboard-mcp: shutdown failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
20
23
|
wiring.exit(1);
|
|
21
24
|
});
|
|
@@ -23,3 +26,13 @@ export function stopWhenSessionEnds(wiring) {
|
|
|
23
26
|
wiring.signals.once("SIGTERM", exitAfterStop);
|
|
24
27
|
wiring.signals.once("SIGINT", exitAfterStop);
|
|
25
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* DX-5415: the server's stop callback, the one link from every shutdown trigger to the relay's settle: it returns the relay's
|
|
31
|
+
* pending `close()` so a signal handler can wait for it before exiting.
|
|
32
|
+
*/
|
|
33
|
+
export function makeSessionStop(permissionWatch, planEventRelay) {
|
|
34
|
+
return () => {
|
|
35
|
+
permissionWatch.stop();
|
|
36
|
+
return planEventRelay.close();
|
|
37
|
+
};
|
|
38
|
+
}
|
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.338",
|
|
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",
|