breakpoint-mcp 1.77.0 → 1.78.1
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/bridge.js +39 -11
- package/dist/close-cause.js +98 -0
- package/dist/csdap.js +78 -3
- package/dist/cslsp.js +19 -2
- package/dist/dap.js +19 -3
- package/dist/index.js +1 -1
- package/dist/lsp.js +19 -3
- package/dist/tools/csdap.js +121 -0
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -4,13 +4,23 @@ import { log } from "./logger.js";
|
|
|
4
4
|
import { OverdueLedger } from "./late-reply.js";
|
|
5
5
|
import { findNonFinite, describeNonFinite, tolerate, TOLERANT_METHODS } from "./finiteness.js";
|
|
6
6
|
import { remedyForWireError } from "./remedies.js";
|
|
7
|
+
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
7
8
|
export class BridgeError extends Error {
|
|
8
9
|
code;
|
|
9
10
|
/**
|
|
10
11
|
* The next action the addon attached to this code (254). Optional by
|
|
11
|
-
* construction: the addon omits it for codes it has nothing to say about
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* construction: the addon omits it for codes it has nothing to say about.
|
|
13
|
+
*
|
|
14
|
+
* 🔴 THE REST OF THIS SENTENCE WAS WRONG FOR TWO SESSIONS, AND 264 MEASURED IT. It
|
|
15
|
+
* read: *every failure the host raises itself — timeout, closed socket, non-finite —
|
|
16
|
+
* arrives without one, because the remedy for those is not the addon's to give.* The
|
|
17
|
+
* because-clause is true and the claim does not follow from it. 262 answered `timeout`;
|
|
18
|
+
* 264 answered `bridge_closed` off an errno that had been in hand at the close site the
|
|
19
|
+
* whole time. The census counted the rest: of 25 host-raised failures about the world,
|
|
20
|
+
* ONE carried a remedy in this field, SEVEN named a next action in their message where
|
|
21
|
+
* no reader counts it, and SEVENTEEN said nothing while holding an errno, a state
|
|
22
|
+
* machine or a registry the host owns. **Absent here means nobody has answered it yet,
|
|
23
|
+
* not that it is unanswerable.**
|
|
14
24
|
*/
|
|
15
25
|
remedy;
|
|
16
26
|
constructor(code, message, remedy) {
|
|
@@ -119,14 +129,22 @@ export class BridgeClient {
|
|
|
119
129
|
this.hint = hint;
|
|
120
130
|
this.secretProvider = secretProvider;
|
|
121
131
|
this.ledger = new OverdueLedger("bridge", peerNoun, deadlineKnob);
|
|
132
|
+
// 🔴 KEPT, not just handed to the ledger (264). A dropped connection names the same
|
|
133
|
+
// peer a late reply does, and the two sentences drifting apart would be a bug nobody
|
|
134
|
+
// could see from either side.
|
|
135
|
+
this.peerNoun = peerNoun;
|
|
122
136
|
}
|
|
137
|
+
/** Who answers this client — read by the close path and the late-reply ledger alike. */
|
|
138
|
+
peerNoun;
|
|
123
139
|
/**
|
|
124
140
|
* A cause this client cannot see, supplied by whoever CAN, as the remedy on a timeout.
|
|
125
141
|
*
|
|
126
142
|
* 🔴 WHY THIS EXISTS AT ALL — the eighth session of "the host's own failures carry no
|
|
127
143
|
* remedy" (254 §4.6 → 261 §4.4), answered for the one case a walk actually produced.
|
|
128
144
|
* `BridgeError.remedy` was documented as "the addon's to give, and a host-raised
|
|
129
|
-
* timeout arrives without one".
|
|
145
|
+
* timeout arrives without one". 🔴 264 DISPROVED THE "TRUE FOR A CLOSED SOCKET" HALF —
|
|
146
|
+
* `onClose` had the errno in hand and read only its `message`, so a killed peer and an
|
|
147
|
+
* orderly shutdown arrived as one code and one sentence. NOT true here either, and USER_GUIDE
|
|
130
148
|
* §10 B's own step 5 walks the reader straight into it: the addon services `runtime_*`
|
|
131
149
|
* from `_process`, so a breakpoint inside the method being called halts the very frame
|
|
132
150
|
* that owes the reply. Measured — the stop's stack trace is `compute` on top of
|
|
@@ -328,12 +346,18 @@ export class BridgeClient {
|
|
|
328
346
|
this.socket = null;
|
|
329
347
|
// Name the transport error when there was one. The CODE stays `bridge_closed`
|
|
330
348
|
// — callers and tests branch on it — but the message carries the errno.
|
|
331
|
-
const cause = this.lastSocketError;
|
|
349
|
+
const cause = this.lastSocketError ?? undefined;
|
|
332
350
|
this.lastSocketError = null;
|
|
333
|
-
const detail =
|
|
351
|
+
const detail = closeDetail(cause);
|
|
352
|
+
// 🔴 THE ERRNO WAS ALREADY HERE AND ONLY ITS `message` WAS READ (264 §3). A peer that
|
|
353
|
+
// was killed and a peer that shut down cleanly produced the SAME code and the SAME
|
|
354
|
+
// sentence, differing by one parenthetical nothing instructed the caller to act on.
|
|
355
|
+
// `cause.code` separates them, and this is the one close site whose class has a field
|
|
356
|
+
// to put the answer in — see close-cause.ts for the four that do not.
|
|
357
|
+
const remedy = closeRemedy(cause, this.peerNoun);
|
|
334
358
|
for (const [, p] of this.pending) {
|
|
335
359
|
clearTimeout(p.timer);
|
|
336
|
-
p.reject(new BridgeError("bridge_closed", `Bridge connection closed before a response arrived${detail}
|
|
360
|
+
p.reject(new BridgeError("bridge_closed", `Bridge connection closed before a response arrived${detail}`, remedy));
|
|
337
361
|
}
|
|
338
362
|
this.pending.clear();
|
|
339
363
|
// Keep the push channel alive across editor restarts while subscriptions want it.
|
|
@@ -353,10 +377,14 @@ export class BridgeClient {
|
|
|
353
377
|
// exact `timed out after <n>ms` phrasing — tools/dap.ts:29 and
|
|
354
378
|
// tools/csdap.ts:31 branch on that substring.
|
|
355
379
|
this.ledger.note(id, method, timeoutMs);
|
|
356
|
-
// 🔴 THE ONE HOST-RAISED FAILURE THAT HAS A KNOWABLE CAUSE
|
|
357
|
-
// else this class raises
|
|
358
|
-
//
|
|
359
|
-
//
|
|
380
|
+
// 🔴 262 §2 CALLED THIS "THE ONE HOST-RAISED FAILURE THAT HAS A KNOWABLE CAUSE",
|
|
381
|
+
// and said everything else this class raises — "a closed socket, a peer that never
|
|
382
|
+
// answered" — is genuinely opaque from here. 264 MEASURED THAT AND IT IS FALSE FOR
|
|
383
|
+
// THE CLOSED SOCKET: `onClose` holds the socket `Error` and its errno separates a
|
|
384
|
+
// peer that was killed from one that shut down cleanly. It stays TRUE for this
|
|
385
|
+
// line — a peer that never answered really does leave nothing behind — which is
|
|
386
|
+
// why the one cause the host can supply comes from outside the socket: its own
|
|
387
|
+
// debugger holding the game. See `setHoldProbe`, and 264's census for the rest.
|
|
360
388
|
reject(new BridgeError("timeout", `Bridge request '${method}' timed out after ${timeoutMs}ms`, this.holdProbe?.() ?? undefined));
|
|
361
389
|
}, timeoutMs);
|
|
362
390
|
this.pending.set(id, { resolve: resolve, reject, timer, method });
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a dropped connection's errno discriminates, and the next action for each.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 WHY THIS EXISTS — 264's census, which is the answer to a row nine sessions old.
|
|
5
|
+
* `BridgeError`'s own doc said every failure the host raises "arrives without one,
|
|
6
|
+
* because the remedy for those is not the addon's to give", and `setHoldProbe`'s said
|
|
7
|
+
* that was "true for a closed socket". **It is not true for a closed socket.** Measured
|
|
8
|
+
* against real sockets, three ways a peer can go:
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* peer resetAndDestroy code=ECONNRESET syscall=read message="read ECONNRESET"
|
|
12
|
+
* peer end() cleanly NULL — no socket error at all
|
|
13
|
+
* peer destroy() NULL — no socket error at all
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* and what a caller received for the last two was the SAME code and the SAME sentence,
|
|
17
|
+
* separated by a parenthesis:
|
|
18
|
+
*
|
|
19
|
+
* ```
|
|
20
|
+
* B peer reset mid-request bridge_closed Bridge connection closed before a response arrived (read ECONNRESET)
|
|
21
|
+
* C peer closed cleanly bridge_closed Bridge connection closed before a response arrived
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* An editor that was killed and an addon that shut its socket on purpose are different
|
|
25
|
+
* next actions. The `Error` that tells them apart was already in hand at all five close
|
|
26
|
+
* sites — `framing.ts` hands the whole object to every one of them — and every one read
|
|
27
|
+
* `cause.message` and threw the rest away.
|
|
28
|
+
*
|
|
29
|
+
* 🔴 ONE MODULE, FIVE CALLERS, BECAUSE THE ALTERNATIVE IS FIVE COPIES OF A JUDGEMENT.
|
|
30
|
+
* `bridge.ts`, `dap.ts`, `csdap.ts`, `lsp.ts` and `cslsp.ts` each drop a connection the
|
|
31
|
+
* same way for the same reasons. A sentence pasted into five close handlers is five
|
|
32
|
+
* places for the errno list to drift.
|
|
33
|
+
*
|
|
34
|
+
* 🔴 AND WHAT IT DELIBERATELY DOES NOT DO. An errno outside the abrupt family gets NO
|
|
35
|
+
* remedy — not a hedge, not a generic one. 264 measured two families and can speak for
|
|
36
|
+
* two; `classifyClose` returns `"unclassified"` for the rest so a future session can find
|
|
37
|
+
* the population it did not cover instead of inheriting a sentence nobody measured.
|
|
38
|
+
* 260's rule: a diagnostic that names a state is not a diagnosis of its cause.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Errnos that mean the peer went away WITHOUT closing the connection.
|
|
42
|
+
*
|
|
43
|
+
* ECONNRESET: the peer's stack sent RST — the process died or was killed mid-stream.
|
|
44
|
+
* EPIPE: we wrote to a connection the peer had already torn down.
|
|
45
|
+
* ECONNABORTED: the local stack aborted an established connection.
|
|
46
|
+
*
|
|
47
|
+
* All three say the same thing about the world — nobody hung up, somebody vanished —
|
|
48
|
+
* and none of them can be produced by an orderly shutdown.
|
|
49
|
+
*/
|
|
50
|
+
export const ABRUPT_CLOSE_CODES = new Set(["ECONNRESET", "EPIPE", "ECONNABORTED"]);
|
|
51
|
+
/**
|
|
52
|
+
* Which of the three a close was. `undefined` means the socket ended with no error at
|
|
53
|
+
* all, which is what an orderly `end()` or `destroy()` looks like from this side.
|
|
54
|
+
*/
|
|
55
|
+
export function classifyClose(cause) {
|
|
56
|
+
if (!cause)
|
|
57
|
+
return "deliberate";
|
|
58
|
+
const code = cause.code;
|
|
59
|
+
if (typeof code === "string" && ABRUPT_CLOSE_CODES.has(code))
|
|
60
|
+
return "abrupt";
|
|
61
|
+
return "unclassified";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The next action, given who the peer was — or `undefined` when the errno does not
|
|
65
|
+
* determine one.
|
|
66
|
+
*
|
|
67
|
+
* Written to the grammar check 28 enforces on `error_remedies.gd`: one imperative at the
|
|
68
|
+
* head, a full stop at the end, and nothing named that does not resolve. `peer` is the
|
|
69
|
+
* noun each client already uses for whoever answers it — "the editor", "the running
|
|
70
|
+
* game", "the debug adapter", "the language server" — so the sentence names the thing the
|
|
71
|
+
* caller was actually talking to rather than a socket.
|
|
72
|
+
*/
|
|
73
|
+
export function closeRemedy(cause, peer) {
|
|
74
|
+
switch (classifyClose(cause)) {
|
|
75
|
+
case "abrupt":
|
|
76
|
+
// The errno is NOT repeated here: `closeDetail` has already put it in the sentence
|
|
77
|
+
// immediately to the left, and a caller reading `(read ECONNRESET) — … (ECONNRESET)`
|
|
78
|
+
// learns nothing from the second one.
|
|
79
|
+
return (`Restart ${peer} and retry — it went away without closing the connection, ` +
|
|
80
|
+
`so nothing here says whether this request ran.`);
|
|
81
|
+
case "deliberate":
|
|
82
|
+
return (`Check whether ${peer} is still running — it closed the connection in an orderly way ` +
|
|
83
|
+
`rather than dropping it, which is what a shutdown looks like from here.`);
|
|
84
|
+
default:
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The parenthetical the five close handlers already built by hand, spelled once.
|
|
90
|
+
*
|
|
91
|
+
* Kept byte-identical to what shipped — ` (read ECONNRESET)` — because tests and users
|
|
92
|
+
* read it, and this change is about adding the next action, not about rewording the
|
|
93
|
+
* detail that was already right.
|
|
94
|
+
*/
|
|
95
|
+
export function closeDetail(cause) {
|
|
96
|
+
return cause ? ` (${cause.message})` : "";
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=close-cause.js.map
|
package/dist/csdap.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { DapError } from "./dap.js";
|
|
3
3
|
import { OverdueLedger } from "./late-reply.js";
|
|
4
|
+
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
5
|
+
/**
|
|
6
|
+
* Who answers this plane, in the words its late-reply ledger already uses.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 ONE CONSTANT, TWO READERS (264). The ledger names this peer when a reply arrives
|
|
9
|
+
* late and `closeRemedy` names it when the connection drops. Two literals would let the
|
|
10
|
+
* same peer acquire two names on the same run, which is the class of drift 257's
|
|
11
|
+
* per-instance deadline noun was written to stop.
|
|
12
|
+
*/
|
|
13
|
+
const CSDAP_PEER = "the debug adapter";
|
|
4
14
|
/**
|
|
5
15
|
* How long `start()` waits for the entry `stopped` event when `stopAtEntry` was
|
|
6
16
|
* requested. Bounded so an adapter that ignores stop-on-entry reports `running`
|
|
@@ -41,7 +51,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
41
51
|
* dropped. `seq` is monotonic and is never reset, so a seq is never reused and
|
|
42
52
|
* a late response can only be its own request's.
|
|
43
53
|
*/
|
|
44
|
-
ledger = new OverdueLedger("C# DAP",
|
|
54
|
+
ledger = new OverdueLedger("C# DAP", CSDAP_PEER, "GODOT_CSDAP_TIMEOUT_MS");
|
|
45
55
|
configured = false;
|
|
46
56
|
/** Persistent watch expressions, re-evaluated at each stop (see evaluateWatches). */
|
|
47
57
|
watches = [];
|
|
@@ -53,10 +63,61 @@ export class CsDapClient extends EventEmitter {
|
|
|
53
63
|
* being emitted as an unlistened `error` event (which throws).
|
|
54
64
|
*/
|
|
55
65
|
startFailure = null;
|
|
66
|
+
/**
|
|
67
|
+
* Whether a launch/attach this client sent was accepted. Tracked SEPARATELY from
|
|
68
|
+
* `state` for the reason `DapClient` tracks it separately, measured here on this
|
|
69
|
+
* plane against a real netcoredbg: `cs_dbg_continue` on a client that had never
|
|
70
|
+
* launched anything left `state = "running"` behind — `resume()` assigns it
|
|
71
|
+
* optimistically before it waits — so every call after it read as a live session.
|
|
72
|
+
* A guard built on `state` alone would be poisoned by the very call it exists to
|
|
73
|
+
* refuse.
|
|
74
|
+
*/
|
|
75
|
+
sessionStarted = false;
|
|
56
76
|
capabilities = null;
|
|
57
77
|
state = "disconnected";
|
|
58
78
|
lastStoppedThreadId = null;
|
|
59
79
|
lastStoppedReason = null;
|
|
80
|
+
/**
|
|
81
|
+
* Whether a launch/attach the adapter accepted is still in force. The `cs_dbg_*`
|
|
82
|
+
* tools that can only answer from a live session consult this instead of asking
|
|
83
|
+
* netcoredbg and reporting whatever came back.
|
|
84
|
+
*
|
|
85
|
+
* 🔴 Measured against a real netcoredbg 3.2.0-1092 on a client that had never
|
|
86
|
+
* launched anything: seven of the nine readers answered with a raw hex code —
|
|
87
|
+
* `Failed command 'stackTrace' : 0x80004005` (E_FAIL), `0x80070057` (E_INVALIDARG)
|
|
88
|
+
* — `cs_dbg_set_variable` reported "the debug adapter reported a failure with no
|
|
89
|
+
* message", and `cs_dbg_watch` answered `isError:false` with every entry carrying
|
|
90
|
+
* `error: "error: 0x80004005"`. Only `cs_dbg_restart` refused, and only because it
|
|
91
|
+
* happens to read `lastStartMode`. That is the same single exception session 259
|
|
92
|
+
* measured one plane over, for the same accidental reason.
|
|
93
|
+
*/
|
|
94
|
+
get hasSession() {
|
|
95
|
+
return this.sessionStarted && this.state !== "terminated" && this.state !== "disconnected";
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether the adapter has reported a stop, read through a call so TypeScript's
|
|
99
|
+
* control-flow narrowing does not collapse it — `state` is mutated by the event
|
|
100
|
+
* handler across every `await` in `start()`, so a literal comparison after an
|
|
101
|
+
* assignment is (wrongly) an impossible one to tsc. `DapClient.stoppedNow()`
|
|
102
|
+
* carries the same note for the same reason.
|
|
103
|
+
*/
|
|
104
|
+
stoppedNow() {
|
|
105
|
+
return this.state === "stopped";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Whether there is a FRAME to answer from — the public half of `stoppedNow()`.
|
|
109
|
+
*
|
|
110
|
+
* 🔴 `hasSession` IS NOT THIS, and session 262 spent a whole session on the
|
|
111
|
+
* difference on the GDScript plane. Measured here with a session live and the
|
|
112
|
+
* program merely RUNNING: `stackTrace` 0x80070057, `scopes` / `variables` /
|
|
113
|
+
* `step` 0x80004005, `evaluate` 0x80070057, `set_variable` blaming the adapter,
|
|
114
|
+
* `watch` an `isError:false` answer whose every entry carried a hex code, and
|
|
115
|
+
* `cs_dbg_continue` waiting the full 15 s to answer `{"state":"running"}` —
|
|
116
|
+
* about 45 s of adapter round trips for a question answerable here in none.
|
|
117
|
+
*/
|
|
118
|
+
get isStopped() {
|
|
119
|
+
return this.stoppedNow();
|
|
120
|
+
}
|
|
60
121
|
constructor(channel, timeoutMs) {
|
|
61
122
|
super();
|
|
62
123
|
this.channel = channel;
|
|
@@ -65,10 +126,17 @@ export class CsDapClient extends EventEmitter {
|
|
|
65
126
|
this.channel.onClose((cause) => {
|
|
66
127
|
this.state = "terminated";
|
|
67
128
|
this.configured = false;
|
|
68
|
-
|
|
129
|
+
this.sessionStarted = false;
|
|
130
|
+
const detail = closeDetail(cause);
|
|
131
|
+
// 🔴 THE SAME ERRNO SPLIT `bridge.ts` GOT (264 §3), IN THE MESSAGE BECAUSE THIS CLASS
|
|
132
|
+
// HAS NOWHERE ELSE TO PUT IT. `DapError` carries no `remedy` field, and the plane's
|
|
133
|
+
// `fail()` renders no `remedyClause`, so the next action goes where the caller will
|
|
134
|
+
// actually read it. 264's census records the asymmetry rather than hiding it: of 25
|
|
135
|
+
// host-raised failures about the world, 13 are on classes that cannot carry an answer.
|
|
136
|
+
const remedy = closeRemedy(cause, CSDAP_PEER);
|
|
69
137
|
for (const [, p] of this.pending) {
|
|
70
138
|
clearTimeout(p.timer);
|
|
71
|
-
p.reject(new DapError(p.command, `C# DAP connection closed${detail}`));
|
|
139
|
+
p.reject(new DapError(p.command, `C# DAP connection closed${detail}${remedy ? ` — ${remedy}` : ""}`));
|
|
72
140
|
}
|
|
73
141
|
this.pending.clear();
|
|
74
142
|
this.emit("closed");
|
|
@@ -262,6 +330,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
262
330
|
async start(mode, args) {
|
|
263
331
|
this.lastStartMode = mode;
|
|
264
332
|
this.lastStartArgs = args;
|
|
333
|
+
this.sessionStarted = false;
|
|
265
334
|
// Listen for `initialized` before we ask, so we cannot miss it.
|
|
266
335
|
const onInit = this.waitEvent("initialized", Math.min(this.timeoutMs, 5000));
|
|
267
336
|
this.capabilities = await this.request("initialize", {
|
|
@@ -287,6 +356,9 @@ export class CsDapClient extends EventEmitter {
|
|
|
287
356
|
const startReq = this.request(mode, args).then(() => undefined, (err) => {
|
|
288
357
|
this.startFailure = err;
|
|
289
358
|
this.configured = false;
|
|
359
|
+
// A rejection arriving after `start()` returned ends the session, so the
|
|
360
|
+
// tools' session guard refuses from then on rather than reading `running`.
|
|
361
|
+
this.sessionStarted = false;
|
|
290
362
|
this.state = "terminated";
|
|
291
363
|
// A DISTINCT event name, deliberately: `error` is special-cased by
|
|
292
364
|
// EventEmitter and throws when unlistened. This one is safe to ignore.
|
|
@@ -313,6 +385,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
313
385
|
const fatal = this.startFailure ?? (configureDoneAdvertised ? configureFailure : null);
|
|
314
386
|
if (fatal) {
|
|
315
387
|
this.configured = false;
|
|
388
|
+
this.sessionStarted = false;
|
|
316
389
|
this.state = "terminated";
|
|
317
390
|
void startReq;
|
|
318
391
|
const detail = fatal?.message ?? String(fatal);
|
|
@@ -321,6 +394,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
321
394
|
`${JSON.stringify(args).slice(0, 200)}).`);
|
|
322
395
|
}
|
|
323
396
|
this.configured = true;
|
|
397
|
+
this.sessionStarted = true;
|
|
324
398
|
// 🔴 ONLY from `initialized`, never unconditionally. The adapter can emit `stopped`
|
|
325
399
|
// (or `terminated`) between the configurationDone RESPONSE and this line, and the
|
|
326
400
|
// event handler has already moved the state — a blind `= "running"` clobbers it and
|
|
@@ -430,6 +504,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
430
504
|
this.channel.close();
|
|
431
505
|
this.state = "disconnected";
|
|
432
506
|
this.configured = false;
|
|
507
|
+
this.sessionStarted = false;
|
|
433
508
|
}
|
|
434
509
|
}
|
|
435
510
|
//# sourceMappingURL=csdap.js.map
|
package/dist/cslsp.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { LspError } from "./lsp.js";
|
|
3
3
|
import { OverdueLedger } from "./late-reply.js";
|
|
4
|
+
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
4
5
|
import { packageVersion } from "./version.js";
|
|
6
|
+
/**
|
|
7
|
+
* Who answers this plane, in the words its late-reply ledger already uses.
|
|
8
|
+
*
|
|
9
|
+
* 🔴 ONE CONSTANT, TWO READERS (264). The ledger names this peer when a reply arrives
|
|
10
|
+
* late and `closeRemedy` names it when the connection drops. Two literals would let the
|
|
11
|
+
* same peer acquire two names on the same run, which is the class of drift 257's
|
|
12
|
+
* per-instance deadline noun was written to stop.
|
|
13
|
+
*/
|
|
14
|
+
const CSLSP_PEER = "the language server";
|
|
5
15
|
/**
|
|
6
16
|
* Minimal LSP client for the C#/.NET semantic plane (D4 C2). Transport-agnostic:
|
|
7
17
|
* it drives any `JsonRpcChannel`, so production spawns OmniSharp over stdio
|
|
@@ -31,7 +41,7 @@ export class CsLspClient {
|
|
|
31
41
|
* dropped. `nextId` is monotonic and is deliberately NOT reset by onClose(),
|
|
32
42
|
* so an id is never reused and a late reply can only be its own request's.
|
|
33
43
|
*/
|
|
34
|
-
ledger = new OverdueLedger("C# LSP",
|
|
44
|
+
ledger = new OverdueLedger("C# LSP", CSLSP_PEER, "GODOT_CSLSP_TIMEOUT_MS");
|
|
35
45
|
/** Absolute project root path (no trailing slash), used to canonicalize URIs. */
|
|
36
46
|
rootFsPath;
|
|
37
47
|
constructor(channel, rootUri, timeoutMs) {
|
|
@@ -128,9 +138,16 @@ export class CsLspClient {
|
|
|
128
138
|
}
|
|
129
139
|
}
|
|
130
140
|
onClose(cause) {
|
|
141
|
+
const detail = closeDetail(cause);
|
|
142
|
+
// 🔴 THE SAME ERRNO SPLIT `bridge.ts` GOT (264 §3), IN THE MESSAGE BECAUSE THIS CLASS
|
|
143
|
+
// HAS NOWHERE ELSE TO PUT IT. `LspError` carries no `remedy` field, and the plane's
|
|
144
|
+
// `fail()` renders no `remedyClause`, so the next action goes where the caller will
|
|
145
|
+
// actually read it. 264's census records the asymmetry rather than hiding it: of 25
|
|
146
|
+
// host-raised failures about the world, 13 are on classes that cannot carry an answer.
|
|
147
|
+
const remedy = closeRemedy(cause, CSLSP_PEER);
|
|
131
148
|
for (const [, p] of this.pending) {
|
|
132
149
|
clearTimeout(p.timer);
|
|
133
|
-
p.reject(new LspError("closed", `C# LSP connection closed${
|
|
150
|
+
p.reject(new LspError("closed", `C# LSP connection closed${detail}${remedy ? ` — ${remedy}` : ""}`));
|
|
134
151
|
}
|
|
135
152
|
this.pending.clear();
|
|
136
153
|
this.initialized = null;
|
package/dist/dap.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { FramedConnection } from "./framing.js";
|
|
3
3
|
import { OverdueLedger } from "./late-reply.js";
|
|
4
|
+
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
5
|
+
/**
|
|
6
|
+
* Who answers this plane, in the words its late-reply ledger already uses.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 ONE CONSTANT, TWO READERS (264). The ledger names this peer when a reply arrives
|
|
9
|
+
* late and `closeRemedy` names it when the connection drops. Two literals would let the
|
|
10
|
+
* same peer acquire two names on the same run, which is the class of drift 257's
|
|
11
|
+
* per-instance deadline noun was written to stop.
|
|
12
|
+
*/
|
|
13
|
+
const DAP_PEER = "the debug adapter";
|
|
4
14
|
export class DapError extends Error {
|
|
5
15
|
command;
|
|
6
16
|
constructor(command, message) {
|
|
@@ -73,7 +83,7 @@ export class DapClient extends EventEmitter {
|
|
|
73
83
|
* dropped. `seq` is monotonic and is never reset, so a seq is never reused and
|
|
74
84
|
* a late response can only be its own request's.
|
|
75
85
|
*/
|
|
76
|
-
ledger = new OverdueLedger("DAP",
|
|
86
|
+
ledger = new OverdueLedger("DAP", DAP_PEER, "GODOT_DAP_TIMEOUT_MS");
|
|
77
87
|
configured = false;
|
|
78
88
|
/** Persistent watch expressions, re-evaluated at each stop (see evaluateWatches). */
|
|
79
89
|
watches = [];
|
|
@@ -141,10 +151,16 @@ export class DapClient extends EventEmitter {
|
|
|
141
151
|
// `cause` is the socket-level error when the drop had one (ECONNRESET, EPIPE).
|
|
142
152
|
// Without it every pending request reported a generic close and the operator
|
|
143
153
|
// could not tell a crashed server from something else holding the port.
|
|
144
|
-
const detail =
|
|
154
|
+
const detail = closeDetail(cause);
|
|
155
|
+
// 🔴 THE SAME ERRNO SPLIT `bridge.ts` GOT (264 §3), IN THE MESSAGE BECAUSE THIS CLASS
|
|
156
|
+
// HAS NOWHERE ELSE TO PUT IT. `DapError` carries no `remedy` field, and the plane's
|
|
157
|
+
// `fail()` renders no `remedyClause`, so the next action goes where the caller will
|
|
158
|
+
// actually read it. 264's census records the asymmetry rather than hiding it: of 25
|
|
159
|
+
// host-raised failures about the world, 13 are on classes that cannot carry an answer.
|
|
160
|
+
const remedy = closeRemedy(cause, DAP_PEER);
|
|
145
161
|
for (const [, p] of this.pending) {
|
|
146
162
|
clearTimeout(p.timer);
|
|
147
|
-
p.reject(new DapError(p.command, `DAP connection closed${detail}`));
|
|
163
|
+
p.reject(new DapError(p.command, `DAP connection closed${detail}${remedy ? ` — ${remedy}` : ""}`));
|
|
148
164
|
}
|
|
149
165
|
this.pending.clear();
|
|
150
166
|
this.emit("closed");
|
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ async function main() {
|
|
|
50
50
|
// so long jobs (export/import/headless script) support poll/await/cancel.
|
|
51
51
|
// D3: also advertise resources.subscribe so clients can subscribe to
|
|
52
52
|
// godot://… resources and receive notifications/resources/updated.
|
|
53
|
-
const server = new McpServer({ name: "breakpoint-mcp", version: "1.
|
|
53
|
+
const server = new McpServer({ name: "breakpoint-mcp", version: "1.78.1" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
|
|
54
54
|
// 208 §7.1 — delete from the wire what the protocol already says: the SDK's
|
|
55
55
|
// draft-07 `$schema` inside every input/output schema (MCP's default dialect is
|
|
56
56
|
// 2020-12, and declaring another is an explicit switch a peer may reject), and
|
package/dist/lsp.js
CHANGED
|
@@ -2,6 +2,16 @@ import { fileURLToPath } from "node:url";
|
|
|
2
2
|
import { FramedConnection } from "./framing.js";
|
|
3
3
|
import { packageVersion } from "./version.js";
|
|
4
4
|
import { OverdueLedger } from "./late-reply.js";
|
|
5
|
+
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
6
|
+
/**
|
|
7
|
+
* Who answers this plane, in the words its late-reply ledger already uses.
|
|
8
|
+
*
|
|
9
|
+
* 🔴 ONE CONSTANT, TWO READERS (264). The ledger names this peer when a reply arrives
|
|
10
|
+
* late and `closeRemedy` names it when the connection drops. Two literals would let the
|
|
11
|
+
* same peer acquire two names on the same run, which is the class of drift 257's
|
|
12
|
+
* per-instance deadline noun was written to stop.
|
|
13
|
+
*/
|
|
14
|
+
const LSP_PEER = "the language server";
|
|
5
15
|
export class LspError extends Error {
|
|
6
16
|
code;
|
|
7
17
|
constructor(code, message) {
|
|
@@ -31,7 +41,7 @@ export class LspClient {
|
|
|
31
41
|
* dropped. `nextId` is monotonic and is deliberately NOT reset by onClose(),
|
|
32
42
|
* so an id is never reused and a late reply can only be its own request's.
|
|
33
43
|
*/
|
|
34
|
-
ledger = new OverdueLedger("LSP",
|
|
44
|
+
ledger = new OverdueLedger("LSP", LSP_PEER, "GODOT_LSP_TIMEOUT_MS");
|
|
35
45
|
/** Absolute project root path (no trailing slash), used to canonicalize URIs. */
|
|
36
46
|
rootFsPath;
|
|
37
47
|
constructor(host, port, rootUri, timeoutMs) {
|
|
@@ -132,10 +142,16 @@ export class LspClient {
|
|
|
132
142
|
// `cause` is the socket-level error when the drop had one (ECONNRESET, EPIPE).
|
|
133
143
|
// Without it every pending request reported a generic close and the operator
|
|
134
144
|
// could not tell a crashed server from something else holding the port.
|
|
135
|
-
const detail =
|
|
145
|
+
const detail = closeDetail(cause);
|
|
146
|
+
// 🔴 THE SAME ERRNO SPLIT `bridge.ts` GOT (264 §3), IN THE MESSAGE BECAUSE THIS CLASS
|
|
147
|
+
// HAS NOWHERE ELSE TO PUT IT. `LspError` carries no `remedy` field, and the plane's
|
|
148
|
+
// `fail()` renders no `remedyClause`, so the next action goes where the caller will
|
|
149
|
+
// actually read it. 264's census records the asymmetry rather than hiding it: of 25
|
|
150
|
+
// host-raised failures about the world, 13 are on classes that cannot carry an answer.
|
|
151
|
+
const remedy = closeRemedy(cause, LSP_PEER);
|
|
136
152
|
for (const [, p] of this.pending) {
|
|
137
153
|
clearTimeout(p.timer);
|
|
138
|
-
p.reject(new LspError("closed", `LSP connection closed${detail}`));
|
|
154
|
+
p.reject(new LspError("closed", `LSP connection closed${detail}${remedy ? ` — ${remedy}` : ""}`));
|
|
139
155
|
}
|
|
140
156
|
this.pending.clear();
|
|
141
157
|
this.initialized = null;
|
package/dist/tools/csdap.js
CHANGED
|
@@ -105,6 +105,67 @@ const CS_DAP_PATHS = {
|
|
|
105
105
|
};
|
|
106
106
|
export function registerCsDapTools(server, dap, cfg) {
|
|
107
107
|
const root = cfg.csDapProjectPath;
|
|
108
|
+
/**
|
|
109
|
+
* Refuse when no launch/attach the adapter accepted is in force.
|
|
110
|
+
*
|
|
111
|
+
* 🔴 Measured against a real netcoredbg 3.2.0-1092 on a client that had never
|
|
112
|
+
* launched anything. Nine readers, and the only one that refused did so by
|
|
113
|
+
* accident:
|
|
114
|
+
*
|
|
115
|
+
* ```
|
|
116
|
+
* cs_dbg_stack_trace 11ms Failed command 'stackTrace' : 0x80004005
|
|
117
|
+
* cs_dbg_scopes 1ms Failed command 'scopes' : 0x80004005
|
|
118
|
+
* cs_dbg_variables 1ms Failed command 'variables' : 0x80004005
|
|
119
|
+
* cs_dbg_evaluate 1ms error: 0x80004005
|
|
120
|
+
* cs_dbg_watch 1ms isError:FALSE, every entry error "error: 0x80004005"
|
|
121
|
+
* cs_dbg_set_variable 1ms "the debug adapter reported a failure with no message"
|
|
122
|
+
* cs_dbg_step 1ms Failed command 'next' : 0x80004005
|
|
123
|
+
* cs_dbg_continue 0ms Failed command 'continue' : 0x80004005
|
|
124
|
+
* cs_dbg_restart 1ms refuses — because it reads `lastStartMode`
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* `0x80004005` is E_FAIL and `0x80070057` is E_INVALIDARG: hex for "no", naming
|
|
128
|
+
* nothing the caller can act on and pointing at the adapter for a state the host
|
|
129
|
+
* already knew. The refusal names the two ways to open a session instead.
|
|
130
|
+
*/
|
|
131
|
+
const requireSession = (tool) => {
|
|
132
|
+
if (dap.hasSession)
|
|
133
|
+
return;
|
|
134
|
+
refuse(`${tool} needs a C# debug session: call cs_dbg_launch or cs_dbg_attach first. ` +
|
|
135
|
+
`There is none, so any state, frame or scope reported here would be invented — ` +
|
|
136
|
+
`this call previously answered with the adapter's raw failure code.`, "no_session");
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Refuse when the session is live but the program is not AT A STOP.
|
|
140
|
+
*
|
|
141
|
+
* 🔴 THE SAME PROXY, ONE PLANE OVER — session 262 §1's whole finding, ported after
|
|
142
|
+
* measuring it here rather than assumed. A session existing is a proxy for a frame
|
|
143
|
+
* existing, and the two part company the moment the program runs on. Measured with
|
|
144
|
+
* a launched session and nothing stopped:
|
|
145
|
+
*
|
|
146
|
+
* ```
|
|
147
|
+
* cs_dbg_stack_trace 2ms Failed command 'stackTrace' : 0x80070057
|
|
148
|
+
* cs_dbg_scopes 1ms Failed command 'scopes' : 0x80004005
|
|
149
|
+
* cs_dbg_variables 1ms Failed command 'variables' : 0x80004005
|
|
150
|
+
* cs_dbg_evaluate 1ms error: 0x80070057
|
|
151
|
+
* cs_dbg_set_variable 1ms "the debug adapter reported a failure with no message"
|
|
152
|
+
* cs_dbg_step 1ms Failed command 'next' : 0x80004005
|
|
153
|
+
* cs_dbg_continue 15,000ms isError:FALSE {"state":"running"}
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* 🔴 RAISED BEFORE THE ADAPTER ROUND TRIP, which is most of the point: that 15 s —
|
|
157
|
+
* with `cs_dbg_restart`'s 30 s beside it — is time the caller spent being told
|
|
158
|
+
* nothing, for a question `dap.isStopped` answers in none. The refusal names the
|
|
159
|
+
* state it READ rather than the state it assumed, and names both ways to reach a
|
|
160
|
+
* stop, because "not stopped" is not itself an instruction.
|
|
161
|
+
*/
|
|
162
|
+
const requireStopped = (tool) => {
|
|
163
|
+
if (dap.isStopped)
|
|
164
|
+
return;
|
|
165
|
+
refuse(`${tool} needs the program stopped at a breakpoint: the C# debug session is live but the program is ${dap.state}. ` +
|
|
166
|
+
`Arm a line with cs_dbg_set_breakpoints and trigger that code path, or step from a stop you already have — ` +
|
|
167
|
+
`nothing here can be read from a program that is still running.`, "not_stopped");
|
|
168
|
+
};
|
|
108
169
|
/**
|
|
109
170
|
* Resolve a breakpoint source path and REFUSE one that cannot carry a breakpoint.
|
|
110
171
|
*
|
|
@@ -272,6 +333,8 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
272
333
|
inputSchema: {},
|
|
273
334
|
}, async () => {
|
|
274
335
|
try {
|
|
336
|
+
requireSession("cs_dbg_continue");
|
|
337
|
+
requireStopped("cs_dbg_continue");
|
|
275
338
|
const r = await dap.resume("continue", { threadId: dap.threadId() }, RESUME_WAIT_MS);
|
|
276
339
|
return ok({ state: r.state, stopped_reason: r.reason });
|
|
277
340
|
}
|
|
@@ -286,6 +349,8 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
286
349
|
inputSchema: { kind: z.enum(["in", "over", "out"]).describe("Step kind") },
|
|
287
350
|
}, async ({ kind }) => {
|
|
288
351
|
try {
|
|
352
|
+
requireSession("cs_dbg_step");
|
|
353
|
+
requireStopped("cs_dbg_step");
|
|
289
354
|
const command = kind === "in" ? "stepIn" : kind === "out" ? "stepOut" : "next";
|
|
290
355
|
const r = await dap.resume(command, { threadId: dap.threadId() }, RESUME_WAIT_MS);
|
|
291
356
|
return ok({ state: r.state, stopped_reason: r.reason });
|
|
@@ -300,6 +365,8 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
300
365
|
inputSchema: { levels: z.number().int().positive().optional().describe("Max frames (default 20)") },
|
|
301
366
|
}, async ({ levels }) => {
|
|
302
367
|
try {
|
|
368
|
+
requireSession("cs_dbg_stack_trace");
|
|
369
|
+
requireStopped("cs_dbg_stack_trace");
|
|
303
370
|
const body = await dap.request("stackTrace", { threadId: dap.threadId(), startFrame: 0, levels: levels ?? 20 });
|
|
304
371
|
const frames = Array.isArray(body["stackFrames"])
|
|
305
372
|
? body["stackFrames"].map((f) => ({
|
|
@@ -318,6 +385,8 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
318
385
|
inputSchema: { frame_id: z.number().int().describe("Frame id from cs_dbg_stack_trace") },
|
|
319
386
|
}, async ({ frame_id }) => {
|
|
320
387
|
try {
|
|
388
|
+
requireSession("cs_dbg_scopes");
|
|
389
|
+
requireStopped("cs_dbg_scopes");
|
|
321
390
|
const body = await dap.request("scopes", { frameId: frame_id });
|
|
322
391
|
const scopes = Array.isArray(body["scopes"])
|
|
323
392
|
? body["scopes"].map((s) => ({ name: s.name ?? "", variables_ref: s.variablesReference ?? 0 }))
|
|
@@ -334,6 +403,8 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
334
403
|
inputSchema: { variables_ref: z.number().int().describe("variablesReference from cs_dbg_scopes or a parent variable") },
|
|
335
404
|
}, async ({ variables_ref }) => {
|
|
336
405
|
try {
|
|
406
|
+
requireSession("cs_dbg_variables");
|
|
407
|
+
requireStopped("cs_dbg_variables");
|
|
337
408
|
const body = await dap.request("variables", { variablesReference: variables_ref });
|
|
338
409
|
const variables = Array.isArray(body["variables"])
|
|
339
410
|
? body["variables"].map((v) => ({
|
|
@@ -357,6 +428,11 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
357
428
|
},
|
|
358
429
|
}, async ({ expression, frame_id, confirm }) => {
|
|
359
430
|
try {
|
|
431
|
+
// 🔴 BEFORE the confirmation prompt, not after. Asking the operator to approve
|
|
432
|
+
// an evaluate that cannot run — and that previously answered `0x80070057` when
|
|
433
|
+
// they approved it — spends their attention on a call already known to fail.
|
|
434
|
+
requireSession("cs_dbg_evaluate");
|
|
435
|
+
requireStopped("cs_dbg_evaluate");
|
|
360
436
|
const blocked = await gate(server, confirm, `Evaluate C# expression in the running game: ${expression}`);
|
|
361
437
|
if (blocked)
|
|
362
438
|
return blocked;
|
|
@@ -393,6 +469,11 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
393
469
|
},
|
|
394
470
|
}, async ({ variables_ref, name, value, confirm }) => {
|
|
395
471
|
try {
|
|
472
|
+
// Before the capability read as well as before the prompt: `capabilities` is
|
|
473
|
+
// null until `initialize`, so with no session the check below is a question
|
|
474
|
+
// asked of nobody.
|
|
475
|
+
requireSession("cs_dbg_set_variable");
|
|
476
|
+
requireStopped("cs_dbg_set_variable");
|
|
396
477
|
if (dap.capabilities && dap.capabilities["supportsSetVariable"] === false) {
|
|
397
478
|
return {
|
|
398
479
|
isError: true,
|
|
@@ -436,12 +517,37 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
436
517
|
},
|
|
437
518
|
}, async ({ add, remove, clear, frame_id }) => {
|
|
438
519
|
try {
|
|
520
|
+
requireSession("cs_dbg_watch");
|
|
521
|
+
// 🔴 THE ONE READER HERE NOT REFUSED WHILE THE PROGRAM RUNS, ON PURPOSE — the
|
|
522
|
+
// same deliberate divergence session 262 made on the GDScript plane, for the
|
|
523
|
+
// same reason. `cs_dbg_watch` is two things at once: it MANAGES a persistent
|
|
524
|
+
// set and it evaluates that set, and this tool's own description teaches
|
|
525
|
+
// arming the set and re-reading it "after a step/continue". Refusing the
|
|
526
|
+
// mutation while the program runs would refuse the documented workflow.
|
|
527
|
+
// The set change is applied; only the VALUES are unavailable, and the entry's
|
|
528
|
+
// own `error` field says which.
|
|
439
529
|
if (clear)
|
|
440
530
|
dap.clearWatches();
|
|
441
531
|
if (remove && remove.length)
|
|
442
532
|
dap.removeWatches(remove);
|
|
443
533
|
if (add && add.length)
|
|
444
534
|
dap.addWatches(add);
|
|
535
|
+
if (!dap.isStopped) {
|
|
536
|
+
// 🔴 What this replaces was worse than empty. Measured against a real
|
|
537
|
+
// netcoredbg with no session and again with the program running: an
|
|
538
|
+
// `isError:false` answer whose every entry carried `error: "error:
|
|
539
|
+
// 0x80004005"` — a hex code offered to the caller as the reason their watch
|
|
540
|
+
// has no value. The reason is that the program is not stopped, the host
|
|
541
|
+
// knows it, and it costs no round trip to say so.
|
|
542
|
+
return ok({
|
|
543
|
+
watches: dap.listWatches().map((expression) => ({
|
|
544
|
+
expression,
|
|
545
|
+
value: "",
|
|
546
|
+
type: "",
|
|
547
|
+
error: `not stopped (${dap.state}) — a watch is evaluated in a stopped frame, so values arrive at the next stop`,
|
|
548
|
+
})),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
445
551
|
// Bound each watch's evaluate to the short deadline (mirrors cs_dbg_evaluate): a watch
|
|
446
552
|
// the adapter never answers fails fast on that entry instead of hanging the full DAP
|
|
447
553
|
// timeout at every stop, while other watches still resolve normally.
|
|
@@ -468,6 +574,21 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
468
574
|
// Per the DAP spec a client should only send setExceptionBreakpoints when the adapter
|
|
469
575
|
// advertised at least one exception filter. Short-circuit with a clear "unsupported"
|
|
470
576
|
// message otherwise, matching the GDScript dbg_set_exception_breakpoints discipline.
|
|
577
|
+
// 🔴 "THE CONNECTED ADAPTER" MUST FIRST BE ONE. `capabilities` is null until
|
|
578
|
+
// `initialize`, so with no session this read found nothing and the tool told
|
|
579
|
+
// the caller their debugger advertises no exception filters. Measured against
|
|
580
|
+
// a real netcoredbg 3.2.0-1092: with no session it answered "unsupported by
|
|
581
|
+
// the connected C# debug adapter (it advertises no exceptionBreakpointFilters)"
|
|
582
|
+
// — while that same adapter advertises `all` and `user-unhandled`, which the
|
|
583
|
+
// cs-dap cohort asserts on every CI leg. A capability read is a question for
|
|
584
|
+
// somebody, and there was nobody. Session 262 §2 fixed this shape one plane
|
|
585
|
+
// over, where the message blamed the user's engine BUILD; here it blamed their
|
|
586
|
+
// debugger for a handshake that never happened.
|
|
587
|
+
//
|
|
588
|
+
// NOT `requireStopped`: exception filters are armed for the FUTURE, so a live
|
|
589
|
+
// running session is a legitimate caller — the same call `dbg_data_breakpoints`
|
|
590
|
+
// gets on the GDScript plane, and for the same reason.
|
|
591
|
+
requireSession("cs_dbg_set_exception_breakpoints");
|
|
471
592
|
const advertised = dap.capabilities?.["exceptionBreakpointFilters"];
|
|
472
593
|
const available_filters = Array.isArray(advertised)
|
|
473
594
|
? advertised.map((f) => ({ filter: f.filter ?? "", label: f.label ?? "" }))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakpoint-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.78.1",
|
|
4
4
|
"description": "MCP server exposing Godot to AI coding assistants over the Model Context Protocol — developed and tested with Claude — across four planes (CLI, live editor, LSP+DAP, runtime) with elicitation-gated destructive tools, long jobs on the formal MCP task model, captured console output, and MCP resources.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|