@thehammer/danx-dashboard-mcp 0.1.335 → 0.1.336

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
@@ -866,6 +866,8 @@ async function main() {
866
866
  stopWhenSessionEnds({
867
867
  server: server.server,
868
868
  stdin: process.stdin,
869
+ signals: process,
870
+ exit: (code) => process.exit(code),
869
871
  stop: () => {
870
872
  permissionWatch.stop();
871
873
  return planEventRelay.close();
@@ -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.
@@ -153,8 +155,12 @@ export class PlanEventRelay {
153
155
  }
154
156
  this.touch();
155
157
  const switching = this.ensureReporter(args.transcriptPath);
156
- if (switching !== null)
158
+ if (switching !== null) {
157
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.
161
+ if (signal?.aborted || this.closed)
162
+ return { events: [] };
163
+ }
158
164
  this.buffer = this.buffer.filter((r) => !acknowledged(r.seq, cursor));
159
165
  if (this.pendingStop !== null) {
160
166
  const stopped = this.pendingStop;
@@ -185,6 +191,7 @@ export class PlanEventRelay {
185
191
  }
186
192
  /** Stop everything: the pending call, the stream, the heartbeat, the transcript tailer. For the server's shutdown. */
187
193
  close() {
194
+ this.closed = true;
188
195
  this.supersedeWaiter();
189
196
  return this.suspend();
190
197
  }
@@ -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 settle is bounded by `bound` (shutdown must not hang
130
- * on a slow dashboard); a drain or flush that fails or is cut off costs only what it had not yet delivered, said on stderr.
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
- await this.flushVia(send);
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,24 @@
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
- * Two triggers, both needed: the transport's `onclose` (an orderly close of the connection) and the end of stdin. The SDK's stdio
6
- * transport does not watch stdin for its end (only a transport `close()` reaches `onclose`), so a client that simply exits
7
- * would leave the process running without the second one.
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. `once`: a second signal finds no listener and takes the
12
+ * default action, the way out when the settle is slow.
8
13
  */
9
14
  export function stopWhenSessionEnds(wiring) {
10
15
  wiring.server.onclose = wiring.stop;
11
16
  wiring.stdin.once("end", wiring.stop);
17
+ const exitAfterStop = () => {
18
+ void Promise.resolve(wiring.stop()).then(() => wiring.exit(0), (err) => {
19
+ process.stderr.write(`danx-dashboard-mcp: shutdown failed: ${err instanceof Error ? err.message : String(err)}\n`);
20
+ wiring.exit(1);
21
+ });
22
+ };
23
+ wiring.signals.once("SIGTERM", exitAfterStop);
24
+ wiring.signals.once("SIGINT", exitAfterStop);
12
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.335",
3
+ "version": "0.1.336",
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",