@thehammer/danx-dashboard-mcp 0.1.335 → 0.1.337
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 +4 -5
- package/dist/plan-events-relay.js +29 -15
- package/dist/session-stats.js +11 -5
- package/dist/shutdown.js +29 -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";
|
|
@@ -866,10 +866,9 @@ async function main() {
|
|
|
866
866
|
stopWhenSessionEnds({
|
|
867
867
|
server: server.server,
|
|
868
868
|
stdin: process.stdin,
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
},
|
|
869
|
+
signals: process,
|
|
870
|
+
exit: (code) => process.exit(code),
|
|
871
|
+
stop: makeSessionStop(permissionWatch, planEventRelay),
|
|
873
872
|
});
|
|
874
873
|
const transport = new StdioServerTransport();
|
|
875
874
|
await server.connect(transport);
|
|
@@ -117,6 +117,8 @@ export class PlanEventRelay {
|
|
|
117
117
|
reporter = null;
|
|
118
118
|
/** DX-5415: the stop of the last suspended reporter (its final settle); resolved when none is running. */
|
|
119
119
|
settling = null;
|
|
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
|
+
closed = false;
|
|
120
122
|
digest = new DigestHold((event) => this.accept(event));
|
|
121
123
|
constructor(deps) {
|
|
122
124
|
this.deps = deps;
|
|
@@ -126,12 +128,12 @@ export class PlanEventRelay {
|
|
|
126
128
|
// Parsed first: a cursor this server did not issue is refused before it changes anything.
|
|
127
129
|
const cursor = args.cursor === null ? null : parseCursor(args.cursor);
|
|
128
130
|
// DX-4233: a request already cancelled (a plugin reload) touches nothing and starts nothing.
|
|
129
|
-
if (signal?.aborted)
|
|
131
|
+
if (signal?.aborted || this.closed)
|
|
130
132
|
return { events: [] };
|
|
131
133
|
// DX-5415: a suspend still storing the old collector's held finals ends before this call starts a new one (bounded inside).
|
|
132
134
|
if (this.settling !== null) {
|
|
133
135
|
await this.settling;
|
|
134
|
-
if (signal?.aborted)
|
|
136
|
+
if (signal?.aborted || this.closed)
|
|
135
137
|
return { events: [] };
|
|
136
138
|
}
|
|
137
139
|
// DX-4233: one wait at a time; the older call answers empty so its caller is never left hanging.
|
|
@@ -152,9 +154,14 @@ export class PlanEventRelay {
|
|
|
152
154
|
this.buffer = this.buffer.map((r) => ({ ...r, recent: boundedIds([...cursor.ids, ...r.recent]) }));
|
|
153
155
|
}
|
|
154
156
|
this.touch();
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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.
|
|
162
|
+
if (signal?.aborted || this.closed)
|
|
163
|
+
return { events: [] };
|
|
164
|
+
}
|
|
158
165
|
this.buffer = this.buffer.filter((r) => !acknowledged(r.seq, cursor));
|
|
159
166
|
if (this.pendingStop !== null) {
|
|
160
167
|
const stopped = this.pendingStop;
|
|
@@ -185,6 +192,7 @@ export class PlanEventRelay {
|
|
|
185
192
|
}
|
|
186
193
|
/** Stop everything: the pending call, the stream, the heartbeat, the transcript tailer. For the server's shutdown. */
|
|
187
194
|
close() {
|
|
195
|
+
this.closed = true;
|
|
188
196
|
this.supersedeWaiter();
|
|
189
197
|
return this.suspend();
|
|
190
198
|
}
|
|
@@ -277,8 +285,12 @@ export class PlanEventRelay {
|
|
|
277
285
|
}
|
|
278
286
|
/** Creates the heartbeat reporter on the first call; a later call's transcript path (a resumed session) is handed to it. */
|
|
279
287
|
ensureReporter(transcriptPath) {
|
|
280
|
-
if (this.reporter !== null)
|
|
281
|
-
|
|
288
|
+
if (this.reporter !== null) {
|
|
289
|
+
const switching = this.reporter.useTranscript(transcriptPath);
|
|
290
|
+
if (switching !== null)
|
|
291
|
+
this.chainSettling(switching);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
282
294
|
this.reporter = new HeartbeatReporter({
|
|
283
295
|
auth: () => {
|
|
284
296
|
const auth = this.deps.auth();
|
|
@@ -295,7 +307,14 @@ export class PlanEventRelay {
|
|
|
295
307
|
announce: (text, source) => this.digest.route({ type: "event", id: null, text, origin: null, scope: "session", urgent: isUrgent({ source }) }),
|
|
296
308
|
forceReconnect: () => this.run?.forceReconnect?.(),
|
|
297
309
|
}, transcriptPath ?? null);
|
|
298
|
-
|
|
310
|
+
}
|
|
311
|
+
/** DX-5415: the one settle chain: the last settle and `stopping` both finish before it clears itself. */
|
|
312
|
+
chainSettling(stopping) {
|
|
313
|
+
const settling = Promise.all([this.settling, stopping]).then(() => {
|
|
314
|
+
if (this.settling === settling)
|
|
315
|
+
this.settling = null;
|
|
316
|
+
});
|
|
317
|
+
this.settling = settling;
|
|
299
318
|
}
|
|
300
319
|
/** Mark the relay in use and make sure the heartbeat is scheduled. */
|
|
301
320
|
touch() {
|
|
@@ -339,13 +358,8 @@ export class PlanEventRelay {
|
|
|
339
358
|
this.run = null;
|
|
340
359
|
// DX-5415: the reporter's stop settles its collector's held sub-agent finals first; callers that can wait (shutdown, the next call) do.
|
|
341
360
|
const stopping = this.reporter?.stop() ?? null;
|
|
342
|
-
if (stopping !== null)
|
|
343
|
-
|
|
344
|
-
if (this.settling === settling)
|
|
345
|
-
this.settling = null;
|
|
346
|
-
});
|
|
347
|
-
this.settling = settling;
|
|
348
|
-
}
|
|
361
|
+
if (stopping !== null)
|
|
362
|
+
this.chainSettling(stopping);
|
|
349
363
|
this.reporter = null;
|
|
350
364
|
this.digest.reset();
|
|
351
365
|
this.dropPlanScoped();
|
package/dist/session-stats.js
CHANGED
|
@@ -101,7 +101,7 @@ export class OperatorSessionStats {
|
|
|
101
101
|
/** One collector flush carried by `send`; a refused or unlanded batch throws inside the collector, which re-queues it. */
|
|
102
102
|
async flushVia(send) {
|
|
103
103
|
const outcome = { carried: false, landed: true, planless: false };
|
|
104
|
-
await this.usage.flush({
|
|
104
|
+
const flushed = await this.usage.flush({
|
|
105
105
|
applyStats: async (_dispatchId, payload) => {
|
|
106
106
|
outcome.carried = true;
|
|
107
107
|
const answer = await send(payload);
|
|
@@ -119,15 +119,17 @@ export class OperatorSessionStats {
|
|
|
119
119
|
return answer.stats;
|
|
120
120
|
},
|
|
121
121
|
}, OPERATOR_DISPATCH_REF);
|
|
122
|
-
return outcome;
|
|
122
|
+
return { ...outcome, outcome: flushed };
|
|
123
123
|
}
|
|
124
124
|
/**
|
|
125
125
|
* Stop tailing. Idempotent; a stopped collector never starts again.
|
|
126
126
|
*
|
|
127
127
|
* DX-5415: a sub-agent's final report is held until its end signal is read, and confirmed only by a LATER poll (DX-5413), so a bare
|
|
128
128
|
* `watcher.stop()` lost any final still held or whose signal was read in the last poll. A collecting watcher is therefore settled first:
|
|
129
|
-
* `drainFinal()`, then one flush carried by `send`, and only then stopped. The
|
|
130
|
-
*
|
|
129
|
+
* `drainFinal()`, then one flush carried by `send`, and only then stopped. The bound only stops WAITING: when it fires, `stop` returns
|
|
130
|
+
* and stops the watcher, but the drain and the flush already under way keep running to their own end (each dashboard request is
|
|
131
|
+
* bounded by its own request timeout), so shutdown never hangs and a cut-off settle can still land late. A drain that throws, a flush
|
|
132
|
+
* that fails or is refused, or a settle cut off costs only what it had not yet delivered, said on stderr.
|
|
131
133
|
*/
|
|
132
134
|
async stop(send, bound) {
|
|
133
135
|
if (this.stopped)
|
|
@@ -145,7 +147,11 @@ export class OperatorSessionStats {
|
|
|
145
147
|
try {
|
|
146
148
|
const settled = (async () => {
|
|
147
149
|
await watcher.drainFinal();
|
|
148
|
-
|
|
150
|
+
// DX-5415: a failed or refused flush re-queues nothing for a collector that is stopping, so it says what it costs.
|
|
151
|
+
const flushed = (await this.flushVia(send)).outcome;
|
|
152
|
+
if (flushed !== "ok")
|
|
153
|
+
process.stderr.write(`operator session stats: the final flush was ${flushed}; held sub-agent finals may be lost
|
|
154
|
+
`);
|
|
149
155
|
return "settled";
|
|
150
156
|
})();
|
|
151
157
|
settled.catch(() => undefined); // a cut-off settle's late failure is already reported by the race below
|
package/dist/shutdown.js
CHANGED
|
@@ -2,11 +2,37 @@
|
|
|
2
2
|
* DX-4523 / DX-4233 - what the server lets go of when its session ends: the permission watch's open stream and the plan event
|
|
3
3
|
* relay (its pending call, stream, heartbeat and tailer), so nothing keeps the process alive after the client is gone.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* transport does not watch stdin for its end
|
|
7
|
-
*
|
|
5
|
+
* Three triggers, all needed: the transport's `onclose` (an orderly close of the connection), the end of stdin (the SDK's stdio
|
|
6
|
+
* transport does not watch stdin for its end, so a client that simply exits would leave the process running without it), and a
|
|
7
|
+
* SIGTERM / SIGINT.
|
|
8
|
+
*
|
|
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
|
+
* 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. 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.
|
|
8
15
|
*/
|
|
9
16
|
export function stopWhenSessionEnds(wiring) {
|
|
10
17
|
wiring.server.onclose = wiring.stop;
|
|
11
18
|
wiring.stdin.once("end", wiring.stop);
|
|
19
|
+
const exitAfterStop = () => {
|
|
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) => {
|
|
22
|
+
process.stderr.write(`danx-dashboard-mcp: shutdown failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
23
|
+
wiring.exit(1);
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
wiring.signals.once("SIGTERM", exitAfterStop);
|
|
27
|
+
wiring.signals.once("SIGINT", exitAfterStop);
|
|
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
|
+
};
|
|
12
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.337",
|
|
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",
|