breakpoint-mcp 1.78.3 โ 1.79.0
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/csdap.js +50 -20
- package/dist/cslsp.js +13 -8
- package/dist/dap.js +118 -19
- package/dist/exit-cause.js +13 -7
- package/dist/index.js +1 -1
- package/dist/lsp.js +28 -9
- package/dist/readiness.js +26 -7
- package/dist/schemas.js +10 -2
- package/dist/timeout-cause.js +48 -0
- package/dist/tools/csdap.js +25 -7
- package/dist/tools/dap.js +30 -5
- package/dist/tools/lsp-common.js +6 -3
- package/dist/tools/processes.js +6 -1
- package/package.json +1 -1
package/dist/csdap.js
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { DapError } from "./dap.js";
|
|
2
|
+
import { DapError, INITIALIZED_WAIT_CEILING_MS } from "./dap.js";
|
|
3
3
|
import { OverdueLedger } from "./late-reply.js";
|
|
4
4
|
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
5
|
+
import { timeoutRemedy } from "./timeout-cause.js";
|
|
5
6
|
/**
|
|
6
7
|
* Who answers this plane, in the words its late-reply ledger already uses.
|
|
7
8
|
*
|
|
8
|
-
* ๐ด ONE CONSTANT,
|
|
9
|
-
* late
|
|
10
|
-
*
|
|
11
|
-
* per-instance deadline noun was
|
|
9
|
+
* ๐ด ONE CONSTANT, THREE READERS (264, 267). The ledger names this peer when a reply
|
|
10
|
+
* arrives late, `closeRemedy` names it when the connection drops, and `timeoutRemedy`
|
|
11
|
+
* names it when a deadline fires. Two literals would let the same peer acquire two names
|
|
12
|
+
* on the same run, which is the class of drift 257's per-instance deadline noun was
|
|
13
|
+
* written to stop.
|
|
12
14
|
*/
|
|
13
15
|
const CSDAP_PEER = "the debug adapter";
|
|
16
|
+
/** The environment variable this plane's deadline comes from โ see `dap.ts`'s twin. */
|
|
17
|
+
const CSDAP_TIMEOUT_KNOB = "GODOT_CSDAP_TIMEOUT_MS";
|
|
18
|
+
/**
|
|
19
|
+
* The next action when the adapter refuses to start a session.
|
|
20
|
+
*
|
|
21
|
+
* ๐ด THE LAST OF 264's SEVENTEEN, and the one that most looked like it already had an
|
|
22
|
+
* answer. The message names the mode and the arguments, which is a description of what
|
|
23
|
+
* was asked, not of what to do next โ measured against a real netcoredbg, the two live
|
|
24
|
+
* causes are a `program` that is not a built assembly and a project that has not been
|
|
25
|
+
* compiled, and neither is visible in the argument dump the message already prints.
|
|
26
|
+
*/
|
|
27
|
+
export const CS_START_REMEDY = "Check that the program names a built .NET assembly and that the project compiles โ the C# debug adapter refused the session, so nothing is under the debugger and no process was left running.";
|
|
28
|
+
/** The next action when `cs_dbg_restart` is called with no launch behind it. */
|
|
29
|
+
export const CS_RESTART_REMEDY = "Call `cs_dbg_launch` or `cs_dbg_attach` first โ a restart reuses the parameters of a launch that already happened, and none has.";
|
|
14
30
|
/**
|
|
15
31
|
* How long `start()` waits for the entry `stopped` event when `stopAtEntry` was
|
|
16
32
|
* requested. Bounded so an adapter that ignores stop-on-entry reports `running`
|
|
@@ -51,7 +67,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
51
67
|
* dropped. `seq` is monotonic and is never reset, so a seq is never reused and
|
|
52
68
|
* a late response can only be its own request's.
|
|
53
69
|
*/
|
|
54
|
-
ledger = new OverdueLedger("C# DAP", CSDAP_PEER,
|
|
70
|
+
ledger = new OverdueLedger("C# DAP", CSDAP_PEER, CSDAP_TIMEOUT_KNOB);
|
|
55
71
|
configured = false;
|
|
56
72
|
/** Persistent watch expressions, re-evaluated at each stop (see evaluateWatches). */
|
|
57
73
|
watches = [];
|
|
@@ -128,15 +144,12 @@ export class CsDapClient extends EventEmitter {
|
|
|
128
144
|
this.configured = false;
|
|
129
145
|
this.sessionStarted = false;
|
|
130
146
|
const detail = closeDetail(cause);
|
|
131
|
-
// ๐ด THE
|
|
132
|
-
//
|
|
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.
|
|
147
|
+
// ๐ด ON THE FIELD, NOT IN THE MESSAGE (267) โ see `dap.ts`'s twin for the whole
|
|
148
|
+
// argument. What a caller receives is byte-identical; the channel is what changed.
|
|
136
149
|
const remedy = closeRemedy(cause, CSDAP_PEER);
|
|
137
150
|
for (const [, p] of this.pending) {
|
|
138
151
|
clearTimeout(p.timer);
|
|
139
|
-
p.reject(new DapError(p.command, `C# DAP connection closed${detail}
|
|
152
|
+
p.reject(new DapError(p.command, `C# DAP connection closed${detail}`, remedy));
|
|
140
153
|
}
|
|
141
154
|
this.pending.clear();
|
|
142
155
|
this.emit("closed");
|
|
@@ -217,7 +230,9 @@ export class CsDapClient extends EventEmitter {
|
|
|
217
230
|
// Remember the seq BEFORE rejecting, so a response already in flight is
|
|
218
231
|
// reconciled rather than dropped as anonymous.
|
|
219
232
|
this.ledger.note(seq, command, timeoutMs);
|
|
220
|
-
|
|
233
|
+
// The `timed out after <n>ms` wording is load-bearing โ `tools/csdap.ts`'s
|
|
234
|
+
// `isDapTimeout` branches on it. The next action rides beside it, not inside it.
|
|
235
|
+
reject(new DapError(command, `C# DAP '${command}' timed out after ${timeoutMs}ms`, timeoutRemedy(CSDAP_PEER, CSDAP_TIMEOUT_KNOB)));
|
|
221
236
|
}, timeoutMs);
|
|
222
237
|
this.pending.set(seq, { command, resolve: resolve, reject, timer });
|
|
223
238
|
this.channel.send({ seq, type: "request", command, arguments: args }).catch((err) => {
|
|
@@ -227,15 +242,23 @@ export class CsDapClient extends EventEmitter {
|
|
|
227
242
|
});
|
|
228
243
|
});
|
|
229
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Wait for `name`, bounded โ resolving **`true` when the event arrived and `false` when
|
|
247
|
+
* the timer fired**. The twin of `dap.ts`'s, changed for the same measured reason (267):
|
|
248
|
+
* a `Promise<void>` made a five-second silence and a prompt event the same observable,
|
|
249
|
+
* so `start()` sent `setBreakpoints` ahead of the event that licenses it and said
|
|
250
|
+
* nothing. Driven against a stub adapter that never emits `initialized`, `start()`
|
|
251
|
+
* returned after 5,003 ms having sent the handshake in the conformant order.
|
|
252
|
+
*/
|
|
230
253
|
waitEvent(name, timeoutMs) {
|
|
231
254
|
return new Promise((resolve) => {
|
|
232
255
|
const timer = setTimeout(() => {
|
|
233
256
|
this.removeListener(name, onEvent);
|
|
234
|
-
resolve();
|
|
257
|
+
resolve(false);
|
|
235
258
|
}, timeoutMs);
|
|
236
259
|
const onEvent = () => {
|
|
237
260
|
clearTimeout(timer);
|
|
238
|
-
resolve();
|
|
261
|
+
resolve(true);
|
|
239
262
|
};
|
|
240
263
|
this.once(name, onEvent);
|
|
241
264
|
});
|
|
@@ -332,7 +355,8 @@ export class CsDapClient extends EventEmitter {
|
|
|
332
355
|
this.lastStartArgs = args;
|
|
333
356
|
this.sessionStarted = false;
|
|
334
357
|
// Listen for `initialized` before we ask, so we cannot miss it.
|
|
335
|
-
const
|
|
358
|
+
const initializedWaitMs = Math.min(this.timeoutMs, INITIALIZED_WAIT_CEILING_MS);
|
|
359
|
+
const onInit = this.waitEvent("initialized", initializedWaitMs);
|
|
336
360
|
this.capabilities = await this.request("initialize", {
|
|
337
361
|
clientID: "breakpoint-mcp",
|
|
338
362
|
clientName: "Godot Breakpoint MCP",
|
|
@@ -364,7 +388,8 @@ export class CsDapClient extends EventEmitter {
|
|
|
364
388
|
// EventEmitter and throws when unlistened. This one is safe to ignore.
|
|
365
389
|
this.emit("start_failed", err);
|
|
366
390
|
});
|
|
367
|
-
|
|
391
|
+
// ๐ด CAPTURED, NOT DISCARDED (267) โ see `dap.ts`'s twin.
|
|
392
|
+
const initializedSeen = await onInit;
|
|
368
393
|
await this.applyAllBreakpoints();
|
|
369
394
|
// Arm the entry-stop wait BEFORE configurationDone is sent: netcoredbg can emit
|
|
370
395
|
// `stopped` before its configurationDone response lands, and a listener attached
|
|
@@ -389,9 +414,13 @@ export class CsDapClient extends EventEmitter {
|
|
|
389
414
|
this.state = "terminated";
|
|
390
415
|
void startReq;
|
|
391
416
|
const detail = fatal?.message ?? String(fatal);
|
|
417
|
+
// ๐ด THE NEXT ACTION MOVED OUT OF THE MESSAGE (267). It read *check that the program
|
|
418
|
+
// exists and is a .NET assembly* mid-sentence, between a state report and an
|
|
419
|
+
// argument dump โ 264's census counted exactly this shape among the SEVEN that
|
|
420
|
+
// named an action nothing structural could read. The message now reports state; the
|
|
421
|
+
// field carries the instruction, and check 28's grammar reader can see it.
|
|
392
422
|
throw new DapError(mode, `the C# debug adapter did not start the session: ${detail}. No debug session is ` +
|
|
393
|
-
`running
|
|
394
|
-
`${JSON.stringify(args).slice(0, 200)}).`);
|
|
423
|
+
`running (${mode} args: ${JSON.stringify(args).slice(0, 200)}).`, CS_START_REMEDY);
|
|
395
424
|
}
|
|
396
425
|
this.configured = true;
|
|
397
426
|
this.sessionStarted = true;
|
|
@@ -411,6 +440,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
411
440
|
// simply reports `running`, as before.
|
|
412
441
|
if (entryStop)
|
|
413
442
|
await entryStop;
|
|
443
|
+
return { initializedSeen, initializedWaitMs };
|
|
414
444
|
}
|
|
415
445
|
threadId() {
|
|
416
446
|
return this.lastStoppedThreadId ?? 1;
|
|
@@ -482,7 +512,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
482
512
|
*/
|
|
483
513
|
async restart(overrideArgs = {}, waitMs = 15000) {
|
|
484
514
|
if (!this.lastStartMode || !this.lastStartArgs) {
|
|
485
|
-
throw new DapError("restart", "no C# debug session to restart
|
|
515
|
+
throw new DapError("restart", "no C# debug session to restart", CS_RESTART_REMEDY);
|
|
486
516
|
}
|
|
487
517
|
const args = { ...this.lastStartArgs, ...overrideArgs };
|
|
488
518
|
if (this.capabilities?.["supportsRestartRequest"] === true) {
|
package/dist/cslsp.js
CHANGED
|
@@ -2,6 +2,7 @@ import { fileURLToPath } from "node:url";
|
|
|
2
2
|
import { LspError } from "./lsp.js";
|
|
3
3
|
import { OverdueLedger } from "./late-reply.js";
|
|
4
4
|
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
5
|
+
import { timeoutRemedy } from "./timeout-cause.js";
|
|
5
6
|
import { packageVersion } from "./version.js";
|
|
6
7
|
/**
|
|
7
8
|
* Who answers this plane, in the words its late-reply ledger already uses.
|
|
@@ -12,6 +13,8 @@ import { packageVersion } from "./version.js";
|
|
|
12
13
|
* per-instance deadline noun was written to stop.
|
|
13
14
|
*/
|
|
14
15
|
const CSLSP_PEER = "the language server";
|
|
16
|
+
/** The environment variable this plane's deadline comes from โ see `dap.ts`'s twin. */
|
|
17
|
+
const CSLSP_TIMEOUT_KNOB = "GODOT_CSLSP_TIMEOUT_MS";
|
|
15
18
|
/**
|
|
16
19
|
* Minimal LSP client for the C#/.NET semantic plane (D4 C2). Transport-agnostic:
|
|
17
20
|
* it drives any `JsonRpcChannel`, so production spawns OmniSharp over stdio
|
|
@@ -41,7 +44,7 @@ export class CsLspClient {
|
|
|
41
44
|
* dropped. `nextId` is monotonic and is deliberately NOT reset by onClose(),
|
|
42
45
|
* so an id is never reused and a late reply can only be its own request's.
|
|
43
46
|
*/
|
|
44
|
-
ledger = new OverdueLedger("C# LSP", CSLSP_PEER,
|
|
47
|
+
ledger = new OverdueLedger("C# LSP", CSLSP_PEER, CSLSP_TIMEOUT_KNOB);
|
|
45
48
|
/** Absolute project root path (no trailing slash), used to canonicalize URIs. */
|
|
46
49
|
rootFsPath;
|
|
47
50
|
constructor(channel, rootUri, timeoutMs) {
|
|
@@ -139,15 +142,17 @@ export class CsLspClient {
|
|
|
139
142
|
}
|
|
140
143
|
onClose(cause) {
|
|
141
144
|
const detail = closeDetail(cause);
|
|
142
|
-
// ๐ด THE SAME ERRNO SPLIT `bridge.ts` GOT (264 ยง3),
|
|
143
|
-
//
|
|
144
|
-
// `fail()` renders no `remedyClause
|
|
145
|
-
//
|
|
146
|
-
//
|
|
145
|
+
// ๐ด THE SAME ERRNO SPLIT `bridge.ts` GOT (264 ยง3), ON THE FIELD RATHER THAN IN THE
|
|
146
|
+
// MESSAGE (267). The comment that stood here said `LspError` "carries no `remedy`
|
|
147
|
+
// field, and the plane's `fail()` renders no `remedyClause`" โ true when written, and
|
|
148
|
+
// the reason 264's census counted this site among the SEVEN that named an action
|
|
149
|
+
// nothing structural could read. Both halves are gone. **What a caller receives is
|
|
150
|
+
// byte-identical** and is asserted so: the message ended in ` โ ${remedy}` and the
|
|
151
|
+
// renderer appends exactly that. A change of channel, not of wording.
|
|
147
152
|
const remedy = closeRemedy(cause, CSLSP_PEER);
|
|
148
153
|
for (const [, p] of this.pending) {
|
|
149
154
|
clearTimeout(p.timer);
|
|
150
|
-
p.reject(new LspError("closed", `C# LSP connection closed${detail}
|
|
155
|
+
p.reject(new LspError("closed", `C# LSP connection closed${detail}`, remedy));
|
|
151
156
|
}
|
|
152
157
|
this.pending.clear();
|
|
153
158
|
this.initialized = null;
|
|
@@ -169,7 +174,7 @@ export class CsLspClient {
|
|
|
169
174
|
// Remember the id BEFORE rejecting, so a reply already in flight is
|
|
170
175
|
// reconciled rather than dropped as anonymous.
|
|
171
176
|
this.ledger.note(id, method, timeoutMs);
|
|
172
|
-
reject(new LspError("timeout", `C# LSP '${method}' timed out after ${timeoutMs}ms
|
|
177
|
+
reject(new LspError("timeout", `C# LSP '${method}' timed out after ${timeoutMs}ms`, timeoutRemedy(CSLSP_PEER, CSLSP_TIMEOUT_KNOB)));
|
|
173
178
|
}, timeoutMs);
|
|
174
179
|
this.pending.set(id, { resolve: resolve, reject, timer });
|
|
175
180
|
this.channel.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
|
package/dist/dap.js
CHANGED
|
@@ -2,23 +2,82 @@ import { EventEmitter } from "node:events";
|
|
|
2
2
|
import { FramedConnection } from "./framing.js";
|
|
3
3
|
import { OverdueLedger } from "./late-reply.js";
|
|
4
4
|
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
5
|
+
import { timeoutRemedy } from "./timeout-cause.js";
|
|
5
6
|
/**
|
|
6
7
|
* Who answers this plane, in the words its late-reply ledger already uses.
|
|
7
8
|
*
|
|
8
|
-
* ๐ด ONE CONSTANT,
|
|
9
|
-
* late
|
|
10
|
-
*
|
|
11
|
-
* per-instance deadline noun was
|
|
9
|
+
* ๐ด ONE CONSTANT, THREE READERS (264, 267). The ledger names this peer when a reply
|
|
10
|
+
* arrives late, `closeRemedy` names it when the connection drops, and `timeoutRemedy`
|
|
11
|
+
* names it when a deadline fires. Two literals would let the same peer acquire two names
|
|
12
|
+
* on the same run, which is the class of drift 257's per-instance deadline noun was
|
|
13
|
+
* written to stop.
|
|
12
14
|
*/
|
|
13
15
|
const DAP_PEER = "the debug adapter";
|
|
16
|
+
/**
|
|
17
|
+
* The environment variable this plane's deadline comes from.
|
|
18
|
+
*
|
|
19
|
+
* ๐ด ONE CONSTANT, TWO READERS, FOR THE REASON DIRECTLY ABOVE. The ledger prints it on a
|
|
20
|
+
* late reply and `timeoutRemedy` tells the caller to raise it; a knob renamed in
|
|
21
|
+
* `config.ts` and updated in one of the two would send an operator to export a variable
|
|
22
|
+
* nothing reads, which is worse than saying nothing.
|
|
23
|
+
*/
|
|
24
|
+
const DAP_TIMEOUT_KNOB = "GODOT_DAP_TIMEOUT_MS";
|
|
14
25
|
export class DapError extends Error {
|
|
15
26
|
command;
|
|
16
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The next action โ the same field `BridgeError` has carried since 254, for the same
|
|
29
|
+
* reason, and ๐ด **ITS ABSENCE WAS THE WHOLE OF THE ROW THIS CLOSES.** 264's census
|
|
30
|
+
* found 13 of 25 host-raised failures sitting on classes with nowhere to put an answer;
|
|
31
|
+
* this class was two of the four. Worse than nowhere: three of the raise sites below
|
|
32
|
+
* DID know the next action and pasted it into the MESSAGE, where nothing structural can
|
|
33
|
+
* read it โ no gate can join it, `fail()` cannot render it under one clause with the
|
|
34
|
+
* rest, and a reword drops it with no test able to tell.
|
|
35
|
+
*
|
|
36
|
+
* Optional by construction, exactly as on `BridgeError`: a rejection RELAYING the
|
|
37
|
+
* adapter's own words has no business inventing a next action on its behalf. Absent
|
|
38
|
+
* here means nobody has answered that site yet, not that it is unanswerable.
|
|
39
|
+
*/
|
|
40
|
+
remedy;
|
|
41
|
+
constructor(command, message, remedy) {
|
|
17
42
|
super(message);
|
|
18
43
|
this.command = command;
|
|
19
44
|
this.name = "DapError";
|
|
45
|
+
if (remedy)
|
|
46
|
+
this.remedy = remedy;
|
|
20
47
|
}
|
|
21
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The ceiling on the wait for `initialized`, shared by both DAP planes.
|
|
51
|
+
*
|
|
52
|
+
* Not a new number โ both clients have used `Math.min(timeoutMs, 5000)` since their
|
|
53
|
+
* handshakes were written. It is named here because 267 made the OUTCOME of that wait
|
|
54
|
+
* something a caller reads, and a window reported in a warning has to be the window that
|
|
55
|
+
* was actually waited.
|
|
56
|
+
*/
|
|
57
|
+
export const INITIALIZED_WAIT_CEILING_MS = 5000;
|
|
58
|
+
/**
|
|
59
|
+
* What a launch/attach result says when the adapter never announced itself.
|
|
60
|
+
*
|
|
61
|
+
* ๐ด REPORTED, NOT REFUSED, AND THE CHOICE IS THE WHOLE OF THE ROW. The wait has resolved
|
|
62
|
+
* on its own timer since the handshake was written, so every adapter in the field has
|
|
63
|
+
* been launching through this path; refusing outright would break working setups to fix a
|
|
64
|
+
* silence. The shape is the one this tool already ships for `stop_on_entry_honored`: name
|
|
65
|
+
* what did not happen, name the symptom to expect, leave the session alone. A stricter
|
|
66
|
+
* refusal remains available and is a separate decision.
|
|
67
|
+
*/
|
|
68
|
+
export function unorderedHandshakeWarning(waitedMs) {
|
|
69
|
+
return (`The debug adapter did not announce itself with an initialized event within ${waitedMs}ms. ` +
|
|
70
|
+
`Breakpoints were applied outside the order the DAP specification requires, so a breakpoint ` +
|
|
71
|
+
`that silently fails to bind is the symptom to expect; the session is otherwise live.`);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The next action when `restart` is called with no launch behind it.
|
|
75
|
+
*
|
|
76
|
+
* A `*_REMEDY` const rather than a literal at the throw site because check 28's grammar
|
|
77
|
+
* reader finds host-side remedies by that naming convention โ a sentence spelled inline
|
|
78
|
+
* is a sentence no gate reads, which is the defect this whole change is about.
|
|
79
|
+
*/
|
|
80
|
+
export const DAP_RESTART_REMEDY = "Call `dbg_launch` or `dbg_attach` first โ a restart reuses the parameters of a launch that already happened, and none has.";
|
|
22
81
|
/**
|
|
23
82
|
* Per-line breakpoint modifier fields, each gated by an adapter capability. Godot
|
|
24
83
|
* advertises all three false AND ignores them โ a "conditional" breakpoint halts
|
|
@@ -83,7 +142,7 @@ export class DapClient extends EventEmitter {
|
|
|
83
142
|
* dropped. `seq` is monotonic and is never reset, so a seq is never reused and
|
|
84
143
|
* a late response can only be its own request's.
|
|
85
144
|
*/
|
|
86
|
-
ledger = new OverdueLedger("DAP", DAP_PEER,
|
|
145
|
+
ledger = new OverdueLedger("DAP", DAP_PEER, DAP_TIMEOUT_KNOB);
|
|
87
146
|
configured = false;
|
|
88
147
|
/** Persistent watch expressions, re-evaluated at each stop (see evaluateWatches). */
|
|
89
148
|
watches = [];
|
|
@@ -152,15 +211,19 @@ export class DapClient extends EventEmitter {
|
|
|
152
211
|
// Without it every pending request reported a generic close and the operator
|
|
153
212
|
// could not tell a crashed server from something else holding the port.
|
|
154
213
|
const detail = closeDetail(cause);
|
|
155
|
-
// ๐ด THE SAME ERRNO SPLIT `bridge.ts` GOT (264 ยง3),
|
|
156
|
-
//
|
|
157
|
-
// `fail()` renders no `remedyClause`, so the next
|
|
158
|
-
//
|
|
159
|
-
//
|
|
214
|
+
// ๐ด THE SAME ERRNO SPLIT `bridge.ts` GOT (264 ยง3), NOW ON THE FIELD RATHER THAN IN
|
|
215
|
+
// THE MESSAGE (267). The comment that stood here said `DapError` "carries no
|
|
216
|
+
// `remedy` field, and the plane's `fail()` renders no `remedyClause`, so the next
|
|
217
|
+
// action goes where the caller will actually read it" โ true when it was written,
|
|
218
|
+
// and the reason 264's census counted this site among the SEVEN that named an
|
|
219
|
+
// action nothing structural could read. Both halves are gone: the field exists and
|
|
220
|
+
// `fail()` appends the clause. **What a caller receives is byte-identical**, which
|
|
221
|
+
// is asserted rather than assumed โ the message ended in ` โ ${remedy}` and the
|
|
222
|
+
// renderer appends exactly that, so this is a change of channel, not of wording.
|
|
160
223
|
const remedy = closeRemedy(cause, DAP_PEER);
|
|
161
224
|
for (const [, p] of this.pending) {
|
|
162
225
|
clearTimeout(p.timer);
|
|
163
|
-
p.reject(new DapError(p.command, `DAP connection closed${detail}
|
|
226
|
+
p.reject(new DapError(p.command, `DAP connection closed${detail}`, remedy));
|
|
164
227
|
}
|
|
165
228
|
this.pending.clear();
|
|
166
229
|
this.emit("closed");
|
|
@@ -234,7 +297,10 @@ export class DapClient extends EventEmitter {
|
|
|
234
297
|
// Remember the seq BEFORE rejecting, so a response already in flight is
|
|
235
298
|
// reconciled rather than dropped as anonymous.
|
|
236
299
|
this.ledger.note(seq, command, timeoutMs);
|
|
237
|
-
|
|
300
|
+
// The message keeps its exact `timed out after <n>ms` wording โ `tools/dap.ts`'s
|
|
301
|
+
// `isDapTimeout` branches on it and `timeout-caveat.ts` says out loud that this
|
|
302
|
+
// predicate must not be disturbed. The next action rides beside it, not inside it.
|
|
303
|
+
reject(new DapError(command, `DAP '${command}' timed out after ${timeoutMs}ms`, timeoutRemedy(DAP_PEER, DAP_TIMEOUT_KNOB)));
|
|
238
304
|
}, timeoutMs);
|
|
239
305
|
this.pending.set(seq, { command, resolve: resolve, reject, timer });
|
|
240
306
|
this.conn.send({ seq, type: "request", command, arguments: args }).catch((err) => {
|
|
@@ -251,15 +317,32 @@ export class DapClient extends EventEmitter {
|
|
|
251
317
|
recentLateReplies() {
|
|
252
318
|
return this.ledger.recent();
|
|
253
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Wait for `name`, bounded โ resolving **`true` when the event arrived and `false` when
|
|
322
|
+
* the timer fired**.
|
|
323
|
+
*
|
|
324
|
+
* ๐ด THE RETURN VALUE IS THE FIX (267), AND THE OLD SIGNATURE IS WHY THE DEFECT COULD
|
|
325
|
+
* NOT BE SEEN. `Promise<void>` gave both outcomes the same shape, so `await onInit` in
|
|
326
|
+
* `start()` below could not distinguish *the adapter said it was ready* from *five
|
|
327
|
+
* seconds passed*, and the handshake carried on either way โ sending `setBreakpoints`
|
|
328
|
+
* ahead of the event the DAP specification says must precede it, and telling nobody.
|
|
329
|
+
* Driven against a stub adapter that answers `initialize` and never emits
|
|
330
|
+
* `initialized`, `start()` returned after 5,003 ms having sent
|
|
331
|
+
* `initialize -> launch -> setBreakpoints -> configurationDone` in exactly the order a
|
|
332
|
+
* conformant run sends them, with no complaint anywhere.
|
|
333
|
+
*
|
|
334
|
+
* 262's rule, one instance further in: a guard that asks whether the container exists
|
|
335
|
+
* has not asked whether the content is there. A resolve is not an answer.
|
|
336
|
+
*/
|
|
254
337
|
waitEvent(name, timeoutMs) {
|
|
255
338
|
return new Promise((resolve) => {
|
|
256
339
|
const timer = setTimeout(() => {
|
|
257
340
|
this.removeListener(name, onEvent);
|
|
258
|
-
resolve();
|
|
341
|
+
resolve(false);
|
|
259
342
|
}, timeoutMs);
|
|
260
343
|
const onEvent = () => {
|
|
261
344
|
clearTimeout(timer);
|
|
262
|
-
resolve();
|
|
345
|
+
resolve(true);
|
|
263
346
|
};
|
|
264
347
|
this.once(name, onEvent);
|
|
265
348
|
});
|
|
@@ -398,6 +481,13 @@ export class DapClient extends EventEmitter {
|
|
|
398
481
|
* before returning โ see `dbg_launch`'s `stop_on_entry`. Godot 4.7 does not honour
|
|
399
482
|
* `stopOnEntry` at all, so this wait times out there and the caller is told so
|
|
400
483
|
* rather than being handed a bare `running` that reads like it worked.
|
|
484
|
+
*
|
|
485
|
+
* ๐ `initializedSeen` (267) is the SAME DISCIPLINE applied to the wait one line up.
|
|
486
|
+
* The `initialized` wait had been resolving on its own timer since this handshake was
|
|
487
|
+
* written, so a five-second silence and a prompt event were the same observable and the
|
|
488
|
+
* caller was handed a session that read exactly like a conformant one. It is reported,
|
|
489
|
+
* not refused: every adapter that works today keeps working, and the four launch tools
|
|
490
|
+
* say which of the two happened โ the shape `stop_on_entry_honored` already ships in.
|
|
401
491
|
*/
|
|
402
492
|
async start(mode, args, entryStopWaitMs = 0) {
|
|
403
493
|
// Remember how we started so restart() can reuse (or override) these params.
|
|
@@ -408,7 +498,12 @@ export class DapClient extends EventEmitter {
|
|
|
408
498
|
// restart may reach a different build than the one that dropped last time.
|
|
409
499
|
this.droppedModifiers.clear();
|
|
410
500
|
// Listen for `initialized` before we ask, so we cannot miss it.
|
|
411
|
-
|
|
501
|
+
// The window is captured rather than recomputed at the report site: it is
|
|
502
|
+
// `min(timeoutMs, 5000)`, so a caller who raised the deadline gets 5000 and one who
|
|
503
|
+
// lowered it gets their own number, and a warning naming the wrong one is a warning
|
|
504
|
+
// that sends an operator to the wrong knob.
|
|
505
|
+
const initializedWaitMs = Math.min(this.timeoutMs, INITIALIZED_WAIT_CEILING_MS);
|
|
506
|
+
const onInit = this.waitEvent("initialized", initializedWaitMs);
|
|
412
507
|
this.capabilities = await this.request("initialize", {
|
|
413
508
|
clientID: "breakpoint-mcp",
|
|
414
509
|
clientName: "Godot Breakpoint MCP",
|
|
@@ -436,7 +531,11 @@ export class DapClient extends EventEmitter {
|
|
|
436
531
|
});
|
|
437
532
|
// An entry stop can arrive before configurationDone answers โ arm first.
|
|
438
533
|
const entryStop = entryStopWaitMs > 0 ? this.waitEvent("stopped", entryStopWaitMs) : null;
|
|
439
|
-
await onInit
|
|
534
|
+
// ๐ด CAPTURED, NOT DISCARDED (267). `await onInit` threw this away for as long as the
|
|
535
|
+
// handshake has existed, which is why `setBreakpoints` going out ahead of the event
|
|
536
|
+
// that licenses it was invisible: the failing case and the succeeding case returned
|
|
537
|
+
// the same `undefined`.
|
|
538
|
+
const initializedSeen = await onInit;
|
|
440
539
|
await this.applyAllBreakpoints();
|
|
441
540
|
await this.request("configurationDone", {}).catch(() => undefined);
|
|
442
541
|
// ๐ด By the time we get here an already-arrived rejection has ALREADY run its
|
|
@@ -460,7 +559,7 @@ export class DapClient extends EventEmitter {
|
|
|
460
559
|
if (startFailure)
|
|
461
560
|
throw startFailure;
|
|
462
561
|
}
|
|
463
|
-
return { entryStopSeen: this.stoppedNow() };
|
|
562
|
+
return { entryStopSeen: this.stoppedNow(), initializedSeen, initializedWaitMs };
|
|
464
563
|
}
|
|
465
564
|
threadId() {
|
|
466
565
|
return this.lastStoppedThreadId ?? 1;
|
|
@@ -532,7 +631,7 @@ export class DapClient extends EventEmitter {
|
|
|
532
631
|
*/
|
|
533
632
|
async restart(overrideArgs = {}, waitMs = 15000) {
|
|
534
633
|
if (!this.lastStartMode || !this.lastStartArgs) {
|
|
535
|
-
throw new DapError("restart", "no debug session to restart
|
|
634
|
+
throw new DapError("restart", "no debug session to restart", DAP_RESTART_REMEDY);
|
|
536
635
|
}
|
|
537
636
|
const args = { ...this.lastStartArgs, ...overrideArgs };
|
|
538
637
|
const scene = typeof args["scene"] === "string" ? args["scene"] : null;
|
package/dist/exit-cause.js
CHANGED
|
@@ -42,10 +42,12 @@
|
|
|
42
42
|
* question", spelled here as a parameter nobody declared. `exitSignal` is captured now
|
|
43
43
|
* and `describeExit` says `killed by SIGKILL` where the sentence used to print a null.
|
|
44
44
|
*
|
|
45
|
-
*
|
|
46
|
-
* `exit_code: null` for a signal-killed child
|
|
47
|
-
* addition to a shipped tool
|
|
48
|
-
*
|
|
45
|
+
* ๐ข AND THE WIRE CAUGHT UP AT 267. 266 left this capture deliberately internal โ
|
|
46
|
+
* `godot_output` answered `exit_code: null` for a signal-killed child with no `signal`
|
|
47
|
+
* key, because that is a wire addition to a shipped tool and would make a PATCH a MINOR.
|
|
48
|
+
* It was enqueued as `process-output-omits-signal` and paid in the next cut: the tool now
|
|
49
|
+
* answers `signal`, nullable beside `exit_code`, so a caller can tell a child that chose
|
|
50
|
+
* to exit from one the OS killed without parsing a sentence.
|
|
49
51
|
*/
|
|
50
52
|
/** How a managed child ended, in the words a person would use. */
|
|
51
53
|
export function describeExit(code, signal) {
|
|
@@ -84,9 +86,13 @@ export function readinessRemedy(causes) {
|
|
|
84
86
|
const exited = causes.some((c) => c === "exited");
|
|
85
87
|
const silent = causes.some((c) => c === "silent");
|
|
86
88
|
if (exited && silent) {
|
|
87
|
-
return (
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
return (
|
|
90
|
+
// Trimmed at 267 to the ceiling check 28 has always enforced on the addon's own
|
|
91
|
+
// remedies and now enforces here: it was 255 characters, and the two populations it
|
|
92
|
+
// addresses are still both named.
|
|
93
|
+
`Read the output quoted above and fix what stopped the peer(s) that exited, then check ` +
|
|
94
|
+
`the "Breakpoint MCP" plugin for the one(s) that never answered โ two things failed and ` +
|
|
95
|
+
`only the second is the addon.`);
|
|
90
96
|
}
|
|
91
97
|
if (exited) {
|
|
92
98
|
return (`Read the output quoted above and fix what stopped the child โ it exited on its own, ` +
|
package/dist/index.js
CHANGED
|
@@ -52,7 +52,7 @@ async function main() {
|
|
|
52
52
|
// so long jobs (export/import/headless script) support poll/await/cancel.
|
|
53
53
|
// D3: also advertise resources.subscribe so clients can subscribe to
|
|
54
54
|
// godot://โฆ resources and receive notifications/resources/updated.
|
|
55
|
-
const server = new McpServer({ name: "breakpoint-mcp", version: "1.
|
|
55
|
+
const server = new McpServer({ name: "breakpoint-mcp", version: "1.79.0" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
|
|
56
56
|
// 208 ยง7.1 โ delete from the wire what the protocol already says: the SDK's
|
|
57
57
|
// draft-07 `$schema` inside every input/output schema (MCP's default dialect is
|
|
58
58
|
// 2020-12, and declaring another is an explicit switch a peer may reject), and
|
package/dist/lsp.js
CHANGED
|
@@ -3,6 +3,7 @@ import { FramedConnection } from "./framing.js";
|
|
|
3
3
|
import { packageVersion } from "./version.js";
|
|
4
4
|
import { OverdueLedger } from "./late-reply.js";
|
|
5
5
|
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
6
|
+
import { timeoutRemedy } from "./timeout-cause.js";
|
|
6
7
|
/**
|
|
7
8
|
* Who answers this plane, in the words its late-reply ledger already uses.
|
|
8
9
|
*
|
|
@@ -12,12 +13,28 @@ import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
|
12
13
|
* per-instance deadline noun was written to stop.
|
|
13
14
|
*/
|
|
14
15
|
const LSP_PEER = "the language server";
|
|
16
|
+
/** The environment variable this plane's deadline comes from โ see `dap.ts`'s twin. */
|
|
17
|
+
const LSP_TIMEOUT_KNOB = "GODOT_LSP_TIMEOUT_MS";
|
|
15
18
|
export class LspError extends Error {
|
|
16
19
|
code;
|
|
17
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The next action โ the same field `BridgeError` has carried since 254, added here at
|
|
22
|
+
* 267 for the same reason it was added there. ๐ด **ITS ABSENCE WAS THE ROW.** 264's
|
|
23
|
+
* census found 13 of 25 host-raised failures sitting on classes with nowhere to put an
|
|
24
|
+
* answer, and this class was two of the four. The close handler below already KNEW the
|
|
25
|
+
* next action and pasted it into the message, where no gate can join it and `fail()`
|
|
26
|
+
* cannot render it under one clause.
|
|
27
|
+
*
|
|
28
|
+
* Optional by construction, as on `BridgeError`: a rejection relaying the language
|
|
29
|
+
* server's own words has no business inventing an action on its behalf.
|
|
30
|
+
*/
|
|
31
|
+
remedy;
|
|
32
|
+
constructor(code, message, remedy) {
|
|
18
33
|
super(message);
|
|
19
34
|
this.name = "LspError";
|
|
20
35
|
this.code = code;
|
|
36
|
+
if (remedy)
|
|
37
|
+
this.remedy = remedy;
|
|
21
38
|
}
|
|
22
39
|
}
|
|
23
40
|
/**
|
|
@@ -41,7 +58,7 @@ export class LspClient {
|
|
|
41
58
|
* dropped. `nextId` is monotonic and is deliberately NOT reset by onClose(),
|
|
42
59
|
* so an id is never reused and a late reply can only be its own request's.
|
|
43
60
|
*/
|
|
44
|
-
ledger = new OverdueLedger("LSP", LSP_PEER,
|
|
61
|
+
ledger = new OverdueLedger("LSP", LSP_PEER, LSP_TIMEOUT_KNOB);
|
|
45
62
|
/** Absolute project root path (no trailing slash), used to canonicalize URIs. */
|
|
46
63
|
rootFsPath;
|
|
47
64
|
constructor(host, port, rootUri, timeoutMs) {
|
|
@@ -143,15 +160,17 @@ export class LspClient {
|
|
|
143
160
|
// Without it every pending request reported a generic close and the operator
|
|
144
161
|
// could not tell a crashed server from something else holding the port.
|
|
145
162
|
const detail = closeDetail(cause);
|
|
146
|
-
// ๐ด THE SAME ERRNO SPLIT `bridge.ts` GOT (264 ยง3),
|
|
147
|
-
//
|
|
148
|
-
// `fail()` renders no `remedyClause
|
|
149
|
-
//
|
|
150
|
-
//
|
|
163
|
+
// ๐ด THE SAME ERRNO SPLIT `bridge.ts` GOT (264 ยง3), ON THE FIELD RATHER THAN IN THE
|
|
164
|
+
// MESSAGE (267). The comment that stood here said `LspError` "carries no `remedy`
|
|
165
|
+
// field, and the plane's `fail()` renders no `remedyClause`" โ true when written, and
|
|
166
|
+
// the reason 264's census counted this site among the SEVEN that named an action
|
|
167
|
+
// nothing structural could read. Both halves are gone. **What a caller receives is
|
|
168
|
+
// byte-identical** and is asserted so: the message ended in ` โ ${remedy}` and the
|
|
169
|
+
// renderer appends exactly that. A change of channel, not of wording.
|
|
151
170
|
const remedy = closeRemedy(cause, LSP_PEER);
|
|
152
171
|
for (const [, p] of this.pending) {
|
|
153
172
|
clearTimeout(p.timer);
|
|
154
|
-
p.reject(new LspError("closed", `LSP connection closed${detail}
|
|
173
|
+
p.reject(new LspError("closed", `LSP connection closed${detail}`, remedy));
|
|
155
174
|
}
|
|
156
175
|
this.pending.clear();
|
|
157
176
|
this.initialized = null;
|
|
@@ -166,7 +185,7 @@ export class LspClient {
|
|
|
166
185
|
// Remember the id BEFORE rejecting, so a reply already in flight is
|
|
167
186
|
// reconciled rather than dropped as anonymous.
|
|
168
187
|
this.ledger.note(id, method, timeoutMs);
|
|
169
|
-
reject(new LspError("timeout", `LSP '${method}' timed out after ${timeoutMs}ms
|
|
188
|
+
reject(new LspError("timeout", `LSP '${method}' timed out after ${timeoutMs}ms`, timeoutRemedy(LSP_PEER, LSP_TIMEOUT_KNOB)));
|
|
170
189
|
}, timeoutMs);
|
|
171
190
|
this.pending.set(id, { resolve: resolve, reject, timer });
|
|
172
191
|
this.conn.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
|
package/dist/readiness.js
CHANGED
|
@@ -93,16 +93,35 @@ export async function waitForRuntimeBridge(cfg, timeoutMs) {
|
|
|
93
93
|
* message that says what broke has done half the job โ applied to a field rather
|
|
94
94
|
* than to a message: `bridge_ready: false` says what happened, and this says what
|
|
95
95
|
* to do about it.
|
|
96
|
+
*
|
|
97
|
+
* ๐ด BOTH SENTENCES WERE REWRITTEN AT 267, AND A GATE FOUND THEM RATHER THAN A READER.
|
|
98
|
+
* Widening check 28's grammar to the host's OWN remedies โ until then it read only
|
|
99
|
+
* `error_remedies.gd` โ refused both of these at once, and the two refusals were the same
|
|
100
|
+
* two defects this project has now found three times:
|
|
101
|
+
*
|
|
102
|
+
* 1. **They opened by describing the failure.** *The process launched but the runtime
|
|
103
|
+
* bridge โฆ did not answer ping* is a second copy of what `bridge_ready: false` already
|
|
104
|
+
* said. The next action arrived in the last clause, after 200 characters, which is the
|
|
105
|
+
* exact shape 254 moved the addon's remedies away from.
|
|
106
|
+
* 2. ๐ด **AND THE SECOND ASKED A QUESTION OVER TWO CAUSES.** *Is the "Breakpoint MCP"
|
|
107
|
+
* plugin enabled in this project?* โ with *if the project is simply slow to boot* three
|
|
108
|
+
* clauses later, so the sentence named both causes and instructed on neither. That is
|
|
109
|
+
* 266 ยง1's finding, in a different file, surviving the session that fixed it: the
|
|
110
|
+
* question was removed from `peers.ts` and left standing here, because nothing joined
|
|
111
|
+
* the two sites. A gate over the whole population is what joined them.
|
|
112
|
+
*
|
|
113
|
+
* Neither cause can be discriminated from here โ a ping that went unanswered inside the
|
|
114
|
+
* wait says nothing about which โ so the repair is to name both and give one action each,
|
|
115
|
+
* not to guess.
|
|
96
116
|
*/
|
|
97
117
|
export function notReadyRemedy(cfg, waitedMs) {
|
|
98
118
|
if (waitedMs <= 0) {
|
|
99
|
-
return (`
|
|
100
|
-
|
|
101
|
-
`
|
|
119
|
+
return (`Call \`runtime_get_tree\` to find out whether the runtime bridge at ` +
|
|
120
|
+
`${cfg.runtimeHost}:${cfg.runtimePort} is up, or re-run with a wait above 0 โ the process launched ` +
|
|
121
|
+
`and wait_timeout_ms 0 asked for no wait, so this is unknown, not failed.`);
|
|
102
122
|
}
|
|
103
|
-
return (`
|
|
104
|
-
`
|
|
105
|
-
`
|
|
106
|
-
`raise the wait with wait_timeout_ms.`);
|
|
123
|
+
return (`Raise wait_timeout_ms and retry if the project is slow to boot, and check the "Breakpoint MCP" ` +
|
|
124
|
+
`plugin is enabled if it is not โ the bridge at ${cfg.runtimeHost}:${cfg.runtimePort} answered no ping ` +
|
|
125
|
+
`in ${waitedMs} ms and this host cannot tell those apart.`);
|
|
107
126
|
}
|
|
108
127
|
//# sourceMappingURL=readiness.js.map
|
package/dist/schemas.js
CHANGED
|
@@ -113,6 +113,10 @@ export const outputSchemas = {
|
|
|
113
113
|
id: z.string(),
|
|
114
114
|
exited: z.boolean(),
|
|
115
115
|
exit_code: z.number().nullable(),
|
|
116
|
+
// The signal that ended the child, when one did โ null otherwise, and null for a
|
|
117
|
+
// process still running. Nullable rather than optional, matching exit_code beside it:
|
|
118
|
+
// a key that vanishes cannot be told apart from a host too old to send it.
|
|
119
|
+
signal: z.string().nullable(),
|
|
116
120
|
latest_seq: z.number(),
|
|
117
121
|
lines: z.array(z.object({ seq: z.number(), stream: z.enum(["stdout", "stderr"]), text: z.string() })),
|
|
118
122
|
},
|
|
@@ -449,6 +453,9 @@ export const outputSchemas = {
|
|
|
449
453
|
// one inside a shape makes contract_check.py swallow every later entry into
|
|
450
454
|
// this one, and the drift error then names every field in the file.
|
|
451
455
|
stop_on_entry_honored: z.boolean().optional(),
|
|
456
|
+
// Whether the adapter emitted its initialized event before breakpoints were applied.
|
|
457
|
+
// Always present: false means the handshake ran out of the order DAP specifies.
|
|
458
|
+
initialized_seen: z.boolean(),
|
|
452
459
|
// Modifiers dropped when the handshake applied the BUFFERED breakpoints. Buffered
|
|
453
460
|
// modifiers cannot be feature-detected at set time, so this is where their fate is
|
|
454
461
|
// reported; dbg_set_breakpoints said only that detection was deferred.
|
|
@@ -457,6 +464,7 @@ export const outputSchemas = {
|
|
|
457
464
|
},
|
|
458
465
|
dbg_attach: {
|
|
459
466
|
session_id: z.string(), state: z.string(),
|
|
467
|
+
initialized_seen: z.boolean(),
|
|
460
468
|
unsupported_modifiers: z.array(z.string()).optional(),
|
|
461
469
|
warning: z.string().optional(),
|
|
462
470
|
},
|
|
@@ -496,8 +504,8 @@ export const outputSchemas = {
|
|
|
496
504
|
unresolved: z.array(z.object({ name: z.string(), reason: z.string() })),
|
|
497
505
|
},
|
|
498
506
|
// ---- Plane D: C# debugging / netcoredbg DAP (tools/csdap.ts) ----
|
|
499
|
-
cs_dbg_launch: { session_id: z.string(), state: z.string() },
|
|
500
|
-
cs_dbg_attach: { session_id: z.string(), state: z.string() },
|
|
507
|
+
cs_dbg_launch: { session_id: z.string(), state: z.string(), initialized_seen: z.boolean(), warning: z.string().optional() },
|
|
508
|
+
cs_dbg_attach: { session_id: z.string(), state: z.string(), initialized_seen: z.boolean(), warning: z.string().optional() },
|
|
501
509
|
cs_dbg_set_breakpoints: {
|
|
502
510
|
path: z.string(), buffered: z.boolean(),
|
|
503
511
|
breakpoints: z.array(z.object({ line: z.number(), verified: z.boolean() })),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The next action when a request hit its own deadline โ the last class of host-raised
|
|
3
|
+
* failure on the DAP and LSP planes that had nothing to say.
|
|
4
|
+
*
|
|
5
|
+
* ๐ด WHY THIS EXISTS, AND WHY IT IS THE END OF A ROW RATHER THAN THE MIDDLE OF ONE.
|
|
6
|
+
* 264's census counted 25 host-raised failures about the world: ONE carried a remedy in
|
|
7
|
+
* a field, SEVEN named a next action inside their message where no reader counts it, and
|
|
8
|
+
* SEVENTEEN said nothing at all. 266 answered the last three that had a field to put an
|
|
9
|
+
* answer in, and left five that did not โ four request deadlines and the C# session
|
|
10
|
+
* start โ because `DapError` and `LspError` had no `remedy` at all. **The gap was never
|
|
11
|
+
* that nobody knew the answer; it was that there was nowhere to put it.** This module is
|
|
12
|
+
* the answer for four of the five, and the field it rides on is the other half.
|
|
13
|
+
*
|
|
14
|
+
* ๐ด WHAT THE HOST ACTUALLY KNOWS AT THE MOMENT A DEADLINE FIRES, which is the whole
|
|
15
|
+
* argument for the sentence below. A dropped connection does NOT reach here: every one of
|
|
16
|
+
* the four clients rejects its pending map from the close handler, with `closeDetail` and
|
|
17
|
+
* `closeRemedy` (264), on a different code path. An unreachable peer does not reach here
|
|
18
|
+
* either โ `connect()` fails earlier and differently. By the time a deadline can fire the
|
|
19
|
+
* connection was established and the frame was written, so the honest reading is *the
|
|
20
|
+
* peer has this request and has not answered yet*, not *the request did not go through*.
|
|
21
|
+
* That is `timeout-caveat.ts`'s measured argument for the editor bridge, made one
|
|
22
|
+
* transport over, where it holds for the same structural reason.
|
|
23
|
+
*
|
|
24
|
+
* ๐ด AND WHAT IT DELIBERATELY DOES NOT SAY. It does not tell the caller to retry. Three
|
|
25
|
+
* of the four planes carry mutating requests (`setVariable`, `textDocument/rename`,
|
|
26
|
+
* `evaluate` with a side effect), and the ledger in `late-reply.ts` exists because a
|
|
27
|
+
* reply arriving after its deadline is ordinary here, not exotic โ so an unqualified
|
|
28
|
+
* *try again* is the instruction that duplicates the write. The sentence names the knob
|
|
29
|
+
* and says plainly that a retry is a second request. 264's rule: measure N families,
|
|
30
|
+
* speak for N.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* The next action for a request that outlived its deadline.
|
|
34
|
+
*
|
|
35
|
+
* `peer` is the noun the plane's own late-reply ledger already uses โ "the debug
|
|
36
|
+
* adapter", "the language server", "the C# debug adapter", "the C# language server" โ
|
|
37
|
+
* so the sentence names what the caller was talking to rather than a socket, exactly as
|
|
38
|
+
* `closeRemedy` does. `knob` is the environment variable `config.ts` reads that
|
|
39
|
+
* deadline from, so the instruction is executable rather than a suggestion to be patient.
|
|
40
|
+
*
|
|
41
|
+
* Written to the grammar check 28 enforces on `error_remedies.gd`: one imperative at the
|
|
42
|
+
* head, a full stop at the end, and nothing named that does not resolve.
|
|
43
|
+
*/
|
|
44
|
+
export function timeoutRemedy(peer, knob) {
|
|
45
|
+
return (`Raise \`${knob}\` if ${peer} is merely slow โ the request reached it and this ` +
|
|
46
|
+
`host's deadline fired first, so it may still answer and a retry sends a second request.`);
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=timeout-cause.js.map
|
package/dist/tools/csdap.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { DapError } from "../dap.js";
|
|
2
|
+
import { DapError, unorderedHandshakeWarning } from "../dap.js";
|
|
3
|
+
import { remedyClause } from "../bridge.js";
|
|
3
4
|
import { resolveSourceFile } from "../paths.js";
|
|
4
5
|
import { gate } from "../confirm.js";
|
|
5
6
|
import { ok } from "./lsp-common.js";
|
|
@@ -32,15 +33,28 @@ function fail(err) {
|
|
|
32
33
|
content: [{
|
|
33
34
|
type: "text",
|
|
34
35
|
text: `C# DAP error [${command}]: the debug adapter reported a failure with no message. ` +
|
|
35
|
-
`The request was rejected; nothing was changed by it
|
|
36
|
+
`The request was rejected; nothing was changed by it.${remedyClause(err)}`,
|
|
36
37
|
}],
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
return {
|
|
40
41
|
isError: true,
|
|
41
|
-
content: [{ type: "text", text: `C# DAP error [${command}]: ${message}` }],
|
|
42
|
+
content: [{ type: "text", text: `C# DAP error [${command}]: ${message}${remedyClause(err)}` }],
|
|
42
43
|
};
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Record whether the adapter announced itself before its breakpoints were applied โ the
|
|
47
|
+
* twin of `tools/dap.ts`'s, for the same measured reason (267). Kept per-plane rather than
|
|
48
|
+
* shared because the two planes' result objects are built by different code and a helper
|
|
49
|
+
* imported across them would be the only thing they share.
|
|
50
|
+
*/
|
|
51
|
+
function reportHandshakeOrder(result, seen, waitedMs) {
|
|
52
|
+
result.initialized_seen = seen;
|
|
53
|
+
if (seen)
|
|
54
|
+
return;
|
|
55
|
+
const note = unorderedHandshakeWarning(waitedMs);
|
|
56
|
+
result.warning = typeof result.warning === "string" && result.warning.length > 0 ? `${result.warning} ${note}` : note;
|
|
57
|
+
}
|
|
44
58
|
/** Throw a host refusal โ rendered verbatim by `fail()`, never as an adapter error. */
|
|
45
59
|
function refuse(message, code) {
|
|
46
60
|
throw Object.assign(new Error(message), { refusal: true, code });
|
|
@@ -239,14 +253,16 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
239
253
|
};
|
|
240
254
|
}
|
|
241
255
|
try {
|
|
242
|
-
await dap.start("launch", {
|
|
256
|
+
const { initializedSeen, initializedWaitMs } = await dap.start("launch", {
|
|
243
257
|
program: resolvedProgram,
|
|
244
258
|
args: resolvedArgs,
|
|
245
259
|
cwd: cfg.csDapProjectPath,
|
|
246
260
|
stopAtEntry: stop_on_entry ?? false,
|
|
247
261
|
justMyCode: just_my_code ?? true,
|
|
248
262
|
});
|
|
249
|
-
|
|
263
|
+
const result = { session_id: "csharp", state: dap.state };
|
|
264
|
+
reportHandshakeOrder(result, initializedSeen, initializedWaitMs);
|
|
265
|
+
return ok(result);
|
|
250
266
|
}
|
|
251
267
|
catch (err) {
|
|
252
268
|
return fail(err);
|
|
@@ -280,8 +296,10 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
280
296
|
`was previously reported as "running" against a process that does not exist.`, "no_such_process");
|
|
281
297
|
}
|
|
282
298
|
}
|
|
283
|
-
await dap.start("attach", { processId: process_id });
|
|
284
|
-
|
|
299
|
+
const { initializedSeen, initializedWaitMs } = await dap.start("attach", { processId: process_id });
|
|
300
|
+
const result = { session_id: "csharp", state: dap.state };
|
|
301
|
+
reportHandshakeOrder(result, initializedSeen, initializedWaitMs);
|
|
302
|
+
return ok(result);
|
|
285
303
|
}
|
|
286
304
|
catch (err) {
|
|
287
305
|
return fail(err);
|
package/dist/tools/dap.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { DapError } from "../dap.js";
|
|
2
|
+
import { DapError, unorderedHandshakeWarning } from "../dap.js";
|
|
3
|
+
import { remedyClause } from "../bridge.js";
|
|
3
4
|
import { resolveSourceFile } from "../paths.js";
|
|
4
5
|
import { gate } from "../confirm.js";
|
|
5
6
|
import { ok } from "./lsp-common.js";
|
|
@@ -116,15 +117,20 @@ function fail(err) {
|
|
|
116
117
|
// asked. Same distinction lsp-common.fail() already draws for the LSP planes.
|
|
117
118
|
const e = err;
|
|
118
119
|
if (err instanceof DapRefusal || e?.refusal) {
|
|
119
|
-
return { isError: true, content: [{ type: "text", text: e.message ?? String(err) }] };
|
|
120
|
+
return { isError: true, content: [{ type: "text", text: `${e.message ?? String(err)}${remedyClause(err)}` }] };
|
|
120
121
|
}
|
|
121
122
|
// An adapter can answer a failure with an EMPTY message (netcoredbg did, on
|
|
122
123
|
// setVariable). `?? String(err)` only covers an ABSENT one, so the whole answer
|
|
123
124
|
// became a label and a colon.
|
|
124
125
|
const detail = e.message && e.message.trim() !== "" ? e.message : String(err);
|
|
126
|
+
// ๐ 267 โ `remedyClause(err)` reads the `remedy` FIELD, which `DapError` gained this
|
|
127
|
+
// release. Before it, this plane's next actions were pasted inside the message at the
|
|
128
|
+
// raise site and this renderer had nothing to append; check 28 could not see them and a
|
|
129
|
+
// reword dropped one silently. Empty string when the site has no answer, so a rejection
|
|
130
|
+
// relaying the adapter's own words is byte-identical to what it was.
|
|
125
131
|
return {
|
|
126
132
|
isError: true,
|
|
127
|
-
content: [{ type: "text", text: `DAP error [${e.command ?? "error"}]: ${detail}` }],
|
|
133
|
+
content: [{ type: "text", text: `DAP error [${e.command ?? "error"}]: ${detail}${remedyClause(err)}` }],
|
|
128
134
|
};
|
|
129
135
|
}
|
|
130
136
|
/**
|
|
@@ -144,6 +150,23 @@ function isDapTimeout(err) {
|
|
|
144
150
|
* it, the breakpoint halted every frame, and the result carried no warning at all.
|
|
145
151
|
* Detection now happens where the modifiers go on the wire, so every path gets it.
|
|
146
152
|
*/
|
|
153
|
+
/**
|
|
154
|
+
* Record whether the adapter announced itself before its breakpoints were applied.
|
|
155
|
+
*
|
|
156
|
+
* ๐ด THE KEY IS ALWAYS PRESENT, THE WARNING ONLY WHEN IT IS FALSE (267). A field that
|
|
157
|
+
* appears only on the bad path is a field a caller cannot branch on โ it cannot tell
|
|
158
|
+
* *this adapter was conformant* from *this host is too old to say* โ which is the shape
|
|
159
|
+
* `required-any-reachability` is open about one layer up. The warning is the human half
|
|
160
|
+
* and merges rather than clobbers, for the same reason `reportDroppedModifiers` does:
|
|
161
|
+
* an out-of-order handshake and an unhonoured stop-on-entry can both be true at once.
|
|
162
|
+
*/
|
|
163
|
+
function reportHandshakeOrder(result, seen, waitedMs) {
|
|
164
|
+
result.initialized_seen = seen;
|
|
165
|
+
if (seen)
|
|
166
|
+
return;
|
|
167
|
+
const note = unorderedHandshakeWarning(waitedMs);
|
|
168
|
+
result.warning = typeof result.warning === "string" && result.warning.length > 0 ? `${result.warning} ${note}` : note;
|
|
169
|
+
}
|
|
147
170
|
const warnDropped = (dropped) => `The connected Godot debug adapter does not support ${dropped.join(", ")} on breakpoints (it advertises ` +
|
|
148
171
|
`${dropped.length > 1 ? "them" : "it"} unsupported), so ${dropped.length > 1 ? "they were" : "it was"} dropped โ ` +
|
|
149
172
|
`the affected breakpoint(s) will halt unconditionally.`;
|
|
@@ -312,8 +335,9 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
312
335
|
};
|
|
313
336
|
}
|
|
314
337
|
try {
|
|
315
|
-
const { entryStopSeen } = await dap.start("launch", { project: cfg.projectPath, scene: scene ?? "main", stopOnEntry: stop_on_entry ?? false }, stop_on_entry ? ENTRY_STOP_WAIT_MS : 0);
|
|
338
|
+
const { entryStopSeen, initializedSeen, initializedWaitMs } = await dap.start("launch", { project: cfg.projectPath, scene: scene ?? "main", stopOnEntry: stop_on_entry ?? false }, stop_on_entry ? ENTRY_STOP_WAIT_MS : 0);
|
|
316
339
|
const result = { session_id: "godot", state: dap.state, scene: scene ?? "main" };
|
|
340
|
+
reportHandshakeOrder(result, initializedSeen, initializedWaitMs);
|
|
317
341
|
if (stop_on_entry) {
|
|
318
342
|
// ๐ด Measured on Godot 4.7: stopOnEntry is ignored outright โ the game ran to
|
|
319
343
|
// completion and printed its _ready() line while the tool answered a bare
|
|
@@ -348,8 +372,9 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
348
372
|
},
|
|
349
373
|
}, async ({ address, port }) => {
|
|
350
374
|
try {
|
|
351
|
-
await dap.start("attach", { address: address ?? "127.0.0.1", port });
|
|
375
|
+
const { initializedSeen, initializedWaitMs } = await dap.start("attach", { address: address ?? "127.0.0.1", port });
|
|
352
376
|
const result = { session_id: "godot", state: dap.state };
|
|
377
|
+
reportHandshakeOrder(result, initializedSeen, initializedWaitMs);
|
|
353
378
|
reportDroppedModifiers(result);
|
|
354
379
|
return ok(result);
|
|
355
380
|
}
|
package/dist/tools/lsp-common.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// MarkupContent), so these live once here rather than being duplicated per plane.
|
|
5
5
|
// The generic MCP success-envelope helper `ok()` is the single shared envelope
|
|
6
6
|
// shape and is imported by the other tool modules too, not only the LSP planes.
|
|
7
|
+
import { remedyClause } from "../bridge.js";
|
|
7
8
|
// LSP CompletionItemKind numeric -> readable name.
|
|
8
9
|
export const COMPLETION_KIND = {
|
|
9
10
|
1: "text", 2: "method", 3: "function", 4: "constructor", 5: "field", 6: "variable",
|
|
@@ -57,12 +58,14 @@ export function fail(err) {
|
|
|
57
58
|
if (e?.refusal) {
|
|
58
59
|
return {
|
|
59
60
|
isError: true,
|
|
60
|
-
content: [{ type: "text", text: e.message ?? String(err) }],
|
|
61
|
+
content: [{ type: "text", text: `${e.message ?? String(err)}${remedyClause(err)}` }],
|
|
61
62
|
};
|
|
62
63
|
}
|
|
64
|
+
// ๐ 267 โ see `tools/dap.ts`. `LspError` gained a `remedy` field this release; this is
|
|
65
|
+
// the one place both LSP planes render, so both get the clause from one edit.
|
|
63
66
|
return {
|
|
64
67
|
isError: true,
|
|
65
|
-
content: [{ type: "text", text: `LSP error [${e.code ?? "error"}]: ${e.message ?? String(err)}` }],
|
|
68
|
+
content: [{ type: "text", text: `LSP error [${e.code ?? "error"}]: ${e.message ?? String(err)}${remedyClause(err)}` }],
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
71
|
/**
|
|
@@ -82,7 +85,7 @@ export function failPath(err) {
|
|
|
82
85
|
const be = err;
|
|
83
86
|
return {
|
|
84
87
|
isError: true,
|
|
85
|
-
content: [{ type: "text", text: `Path error [${be?.code ?? "error"}]: ${be?.message ?? String(err)}` }],
|
|
88
|
+
content: [{ type: "text", text: `Path error [${be?.code ?? "error"}]: ${be?.message ?? String(err)}${remedyClause(err)}` }],
|
|
86
89
|
};
|
|
87
90
|
}
|
|
88
91
|
/**
|
package/dist/tools/processes.js
CHANGED
|
@@ -157,7 +157,12 @@ export function registerProcessTools(server, cfg) {
|
|
|
157
157
|
const since = since_seq ?? 0;
|
|
158
158
|
const want = stream ?? "both";
|
|
159
159
|
const lines = m.lines.filter((l) => l.seq > since && (want === "both" || l.stream === want));
|
|
160
|
-
|
|
160
|
+
// ๐ 267 โ `signal` on the wire. `exit_code` alone could not distinguish a child
|
|
161
|
+
// that chose to exit 0 from one the OS killed: both answered `exited: true` with a
|
|
162
|
+
// null-or-number code, and a SIGKILLed child answered `exit_code: null`, which reads
|
|
163
|
+
// exactly like a process that has not finished. The registry has captured the signal
|
|
164
|
+
// since 266; this is the key that lets a caller read it.
|
|
165
|
+
return ok({ id, exited: m.exited, exit_code: m.exitCode, signal: m.exitSignal, latest_seq: m.seq, lines });
|
|
161
166
|
});
|
|
162
167
|
server.registerTool("godot_stop", {
|
|
163
168
|
title: "Stop managed process",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakpoint-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.79.0",
|
|
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",
|