@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 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 stop of the last suspended reporter (its final settle); resolved when none is running. */
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
- 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
  }
@@ -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
- return this.reporter.useTranscript(transcriptPath);
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
- return null;
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
- const settling = Promise.all([this.settling, stopping]).then(() => {
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`: 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.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",