@thehammer/danx-dashboard-mcp 0.1.336 → 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 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);
@@ -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
- const switching = this.ensureReporter(args.transcriptPath);
158
- if (switching !== null) {
159
- await switching;
160
- // DX-5415 / DX-4233: 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.
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
  }
@@ -284,8 +285,12 @@ export class PlanEventRelay {
284
285
  }
285
286
  /** Creates the heartbeat reporter on the first call; a later call's transcript path (a resumed session) is handed to it. */
286
287
  ensureReporter(transcriptPath) {
287
- if (this.reporter !== null)
288
- return this.reporter.useTranscript(transcriptPath);
288
+ if (this.reporter !== null) {
289
+ const switching = this.reporter.useTranscript(transcriptPath);
290
+ if (switching !== null)
291
+ this.chainSettling(switching);
292
+ return;
293
+ }
289
294
  this.reporter = new HeartbeatReporter({
290
295
  auth: () => {
291
296
  const auth = this.deps.auth();
@@ -302,7 +307,14 @@ export class PlanEventRelay {
302
307
  announce: (text, source) => this.digest.route({ type: "event", id: null, text, origin: null, scope: "session", urgent: isUrgent({ source }) }),
303
308
  forceReconnect: () => this.run?.forceReconnect?.(),
304
309
  }, transcriptPath ?? null);
305
- return null;
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;
306
318
  }
307
319
  /** Mark the relay in use and make sure the heartbeat is scheduled. */
308
320
  touch() {
@@ -346,13 +358,8 @@ export class PlanEventRelay {
346
358
  this.run = null;
347
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.
348
360
  const stopping = this.reporter?.stop() ?? null;
349
- if (stopping !== null) {
350
- const settling = Promise.all([this.settling, stopping]).then(() => {
351
- if (this.settling === settling)
352
- this.settling = null;
353
- });
354
- this.settling = settling;
355
- }
361
+ if (stopping !== null)
362
+ this.chainSettling(stopping);
356
363
  this.reporter = null;
357
364
  this.digest.reset();
358
365
  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`: a second signal finds no listener and takes the
12
- * default action, the way out when the settle is slow.
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
- void Promise.resolve(wiring.stop()).then(() => wiring.exit(0), (err) => {
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.336",
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",