breakpoint-mcp 1.78.3 → 1.80.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/bridge.js +89 -5
- package/dist/config.js +13 -0
- package/dist/csdap.js +65 -22
- package/dist/cslsp.js +13 -8
- package/dist/dap.js +180 -21
- 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 +66 -0
- package/dist/timeout-caveat.js +15 -4
- package/dist/tools/csdap.js +31 -9
- package/dist/tools/dap.js +40 -6
- package/dist/tools/editor/common.js +7 -2
- package/dist/tools/lsp-common.js +6 -3
- package/dist/tools/processes.js +6 -1
- package/package.json +3 -2
package/dist/dap.js
CHANGED
|
@@ -2,23 +2,122 @@ 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, unannouncedRemedy } 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";
|
|
25
|
+
/** This request hit its own deadline. The only code `isDapTimeout` answers to. */
|
|
26
|
+
export const DAP_TIMEOUT_CODE = "timeout";
|
|
27
|
+
/**
|
|
28
|
+
* The adapter answered `initialize` and never emitted `initialized`, so the handshake
|
|
29
|
+
* was refused rather than continued out of order. See `start()`.
|
|
30
|
+
*/
|
|
31
|
+
export const DAP_UNANNOUNCED_CODE = "unannounced";
|
|
14
32
|
export class DapError extends Error {
|
|
15
33
|
command;
|
|
16
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The next action — the same field `BridgeError` has carried since 254, for the same
|
|
36
|
+
* reason, and 🔴 **ITS ABSENCE WAS THE WHOLE OF THE ROW THIS CLOSES.** 264's census
|
|
37
|
+
* found 13 of 25 host-raised failures sitting on classes with nowhere to put an answer;
|
|
38
|
+
* this class was two of the four. Worse than nowhere: three of the raise sites below
|
|
39
|
+
* DID know the next action and pasted it into the MESSAGE, where nothing structural can
|
|
40
|
+
* read it — no gate can join it, `fail()` cannot render it under one clause with the
|
|
41
|
+
* rest, and a reword drops it with no test able to tell.
|
|
42
|
+
*
|
|
43
|
+
* Optional by construction, exactly as on `BridgeError`: a rejection RELAYING the
|
|
44
|
+
* adapter's own words has no business inventing a next action on its behalf. Absent
|
|
45
|
+
* here means nobody has answered that site yet, not that it is unanswerable.
|
|
46
|
+
*/
|
|
47
|
+
remedy;
|
|
48
|
+
/**
|
|
49
|
+
* Why the host raised this, for callers that branch — `undefined` on the six sites
|
|
50
|
+
* that relay the adapter's own failure.
|
|
51
|
+
*
|
|
52
|
+
* 🔴 **`LspError` HAS CARRIED ONE SINCE IT WAS WRITTEN AND THIS CLASS DID NOT, FOR NO
|
|
53
|
+
* REASON ANYBODY DECIDED.** `LspError`'s first constructor argument happens to be a
|
|
54
|
+
* code, so its two timeout sites pass `"timeout"` and every LSP caller branches on a
|
|
55
|
+
* field. This class's first argument is `command`, so its two timeout sites had
|
|
56
|
+
* nowhere to put one — and the tool layer read the message body instead. The shape of
|
|
57
|
+
* a constructor decided whether a shipped branch was structural or textual, and
|
|
58
|
+
* nothing ever asked.
|
|
59
|
+
*/
|
|
60
|
+
code;
|
|
61
|
+
constructor(command, message, remedy, code) {
|
|
17
62
|
super(message);
|
|
18
63
|
this.command = command;
|
|
19
64
|
this.name = "DapError";
|
|
65
|
+
if (remedy)
|
|
66
|
+
this.remedy = remedy;
|
|
67
|
+
if (code)
|
|
68
|
+
this.code = code;
|
|
20
69
|
}
|
|
21
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The ceiling on the wait for `initialized`, shared by both DAP planes.
|
|
73
|
+
*
|
|
74
|
+
* Not a new number — both clients have used `Math.min(timeoutMs, 5000)` since their
|
|
75
|
+
* handshakes were written. It is named here because 267 made the OUTCOME of that wait
|
|
76
|
+
* something a caller reads, and a window reported in a warning has to be the window that
|
|
77
|
+
* was actually waited.
|
|
78
|
+
*/
|
|
79
|
+
export const INITIALIZED_WAIT_CEILING_MS = 5000;
|
|
80
|
+
/**
|
|
81
|
+
* The message both planes raise when the adapter never announces itself.
|
|
82
|
+
*
|
|
83
|
+
* 🔴 ONE BUILDER, TWO PLANES — the same discipline `timeoutRemedy` and `closeRemedy`
|
|
84
|
+
* already follow, and the reason is 267's finding one file over: the SAME question was
|
|
85
|
+
* pasted into `peers.ts` and `readiness.ts`, 266 fixed one, and the other survived the
|
|
86
|
+
* session that fixed it because nothing joined the two sites. A sentence shipped twice
|
|
87
|
+
* is a sentence that will be corrected once.
|
|
88
|
+
*
|
|
89
|
+
* `waitedMs` is the window ACTUALLY waited, not the ceiling — a refusal naming a wait
|
|
90
|
+
* that did not happen sends an operator to the wrong knob, which is exactly why 267
|
|
91
|
+
* captured the window instead of recomputing it at the report site.
|
|
92
|
+
*/
|
|
93
|
+
export function unannouncedMessage(peer, mode, waitedMs) {
|
|
94
|
+
return (`${peer} answered \`initialize\` for this ${mode} and never emitted \`initialized\` ` +
|
|
95
|
+
`within ${waitedMs}ms, so the session was refused rather than configured out of order. ` +
|
|
96
|
+
`No breakpoints were applied and no program was left running by this host.`);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* What a launch/attach result says when the adapter never announced itself.
|
|
100
|
+
*
|
|
101
|
+
* 🔴 REPORTED, NOT REFUSED, AND THE CHOICE IS THE WHOLE OF THE ROW. The wait has resolved
|
|
102
|
+
* on its own timer since the handshake was written, so every adapter in the field has
|
|
103
|
+
* been launching through this path; refusing outright would break working setups to fix a
|
|
104
|
+
* silence. The shape is the one this tool already ships for `stop_on_entry_honored`: name
|
|
105
|
+
* what did not happen, name the symptom to expect, leave the session alone. A stricter
|
|
106
|
+
* refusal remains available and is a separate decision.
|
|
107
|
+
*/
|
|
108
|
+
export function unorderedHandshakeWarning(waitedMs) {
|
|
109
|
+
return (`The debug adapter did not announce itself with an initialized event within ${waitedMs}ms. ` +
|
|
110
|
+
`Breakpoints were applied outside the order the DAP specification requires, so a breakpoint ` +
|
|
111
|
+
`that silently fails to bind is the symptom to expect; the session is otherwise live.`);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The next action when `restart` is called with no launch behind it.
|
|
115
|
+
*
|
|
116
|
+
* A `*_REMEDY` const rather than a literal at the throw site because check 28's grammar
|
|
117
|
+
* reader finds host-side remedies by that naming convention — a sentence spelled inline
|
|
118
|
+
* is a sentence no gate reads, which is the defect this whole change is about.
|
|
119
|
+
*/
|
|
120
|
+
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
121
|
/**
|
|
23
122
|
* Per-line breakpoint modifier fields, each gated by an adapter capability. Godot
|
|
24
123
|
* advertises all three false AND ignores them — a "conditional" breakpoint halts
|
|
@@ -83,7 +182,7 @@ export class DapClient extends EventEmitter {
|
|
|
83
182
|
* dropped. `seq` is monotonic and is never reset, so a seq is never reused and
|
|
84
183
|
* a late response can only be its own request's.
|
|
85
184
|
*/
|
|
86
|
-
ledger = new OverdueLedger("DAP", DAP_PEER,
|
|
185
|
+
ledger = new OverdueLedger("DAP", DAP_PEER, DAP_TIMEOUT_KNOB);
|
|
87
186
|
configured = false;
|
|
88
187
|
/** Persistent watch expressions, re-evaluated at each stop (see evaluateWatches). */
|
|
89
188
|
watches = [];
|
|
@@ -152,15 +251,19 @@ export class DapClient extends EventEmitter {
|
|
|
152
251
|
// Without it every pending request reported a generic close and the operator
|
|
153
252
|
// could not tell a crashed server from something else holding the port.
|
|
154
253
|
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
|
-
//
|
|
254
|
+
// 🔴 THE SAME ERRNO SPLIT `bridge.ts` GOT (264 §3), NOW ON THE FIELD RATHER THAN IN
|
|
255
|
+
// THE MESSAGE (267). The comment that stood here said `DapError` "carries no
|
|
256
|
+
// `remedy` field, and the plane's `fail()` renders no `remedyClause`, so the next
|
|
257
|
+
// action goes where the caller will actually read it" — true when it was written,
|
|
258
|
+
// and the reason 264's census counted this site among the SEVEN that named an
|
|
259
|
+
// action nothing structural could read. Both halves are gone: the field exists and
|
|
260
|
+
// `fail()` appends the clause. **What a caller receives is byte-identical**, which
|
|
261
|
+
// is asserted rather than assumed — the message ended in ` — ${remedy}` and the
|
|
262
|
+
// renderer appends exactly that, so this is a change of channel, not of wording.
|
|
160
263
|
const remedy = closeRemedy(cause, DAP_PEER);
|
|
161
264
|
for (const [, p] of this.pending) {
|
|
162
265
|
clearTimeout(p.timer);
|
|
163
|
-
p.reject(new DapError(p.command, `DAP connection closed${detail}
|
|
266
|
+
p.reject(new DapError(p.command, `DAP connection closed${detail}`, remedy));
|
|
164
267
|
}
|
|
165
268
|
this.pending.clear();
|
|
166
269
|
this.emit("closed");
|
|
@@ -234,7 +337,12 @@ export class DapClient extends EventEmitter {
|
|
|
234
337
|
// Remember the seq BEFORE rejecting, so a response already in flight is
|
|
235
338
|
// reconciled rather than dropped as anonymous.
|
|
236
339
|
this.ledger.note(seq, command, timeoutMs);
|
|
237
|
-
|
|
340
|
+
// 🔴 THE WORDING IS NO LONGER LOAD-BEARING, AND THAT IS 268's CHANGE. It used to
|
|
341
|
+
// be: `tools/dap.ts`'s `isDapTimeout` regexed `/timed out after/` against this
|
|
342
|
+
// string, so a copy edit here silently changed which answer `dbg_evaluate` and
|
|
343
|
+
// `dbg_set_variable` gave, with every test green. The code below is what those
|
|
344
|
+
// branches read now; the sentence is free to be improved.
|
|
345
|
+
reject(new DapError(command, `DAP '${command}' timed out after ${timeoutMs}ms`, timeoutRemedy(DAP_PEER, DAP_TIMEOUT_KNOB), DAP_TIMEOUT_CODE));
|
|
238
346
|
}, timeoutMs);
|
|
239
347
|
this.pending.set(seq, { command, resolve: resolve, reject, timer });
|
|
240
348
|
this.conn.send({ seq, type: "request", command, arguments: args }).catch((err) => {
|
|
@@ -251,15 +359,32 @@ export class DapClient extends EventEmitter {
|
|
|
251
359
|
recentLateReplies() {
|
|
252
360
|
return this.ledger.recent();
|
|
253
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Wait for `name`, bounded — resolving **`true` when the event arrived and `false` when
|
|
364
|
+
* the timer fired**.
|
|
365
|
+
*
|
|
366
|
+
* 🔴 THE RETURN VALUE IS THE FIX (267), AND THE OLD SIGNATURE IS WHY THE DEFECT COULD
|
|
367
|
+
* NOT BE SEEN. `Promise<void>` gave both outcomes the same shape, so `await onInit` in
|
|
368
|
+
* `start()` below could not distinguish *the adapter said it was ready* from *five
|
|
369
|
+
* seconds passed*, and the handshake carried on either way — sending `setBreakpoints`
|
|
370
|
+
* ahead of the event the DAP specification says must precede it, and telling nobody.
|
|
371
|
+
* Driven against a stub adapter that answers `initialize` and never emits
|
|
372
|
+
* `initialized`, `start()` returned after 5,003 ms having sent
|
|
373
|
+
* `initialize -> launch -> setBreakpoints -> configurationDone` in exactly the order a
|
|
374
|
+
* conformant run sends them, with no complaint anywhere.
|
|
375
|
+
*
|
|
376
|
+
* 262's rule, one instance further in: a guard that asks whether the container exists
|
|
377
|
+
* has not asked whether the content is there. A resolve is not an answer.
|
|
378
|
+
*/
|
|
254
379
|
waitEvent(name, timeoutMs) {
|
|
255
380
|
return new Promise((resolve) => {
|
|
256
381
|
const timer = setTimeout(() => {
|
|
257
382
|
this.removeListener(name, onEvent);
|
|
258
|
-
resolve();
|
|
383
|
+
resolve(false);
|
|
259
384
|
}, timeoutMs);
|
|
260
385
|
const onEvent = () => {
|
|
261
386
|
clearTimeout(timer);
|
|
262
|
-
resolve();
|
|
387
|
+
resolve(true);
|
|
263
388
|
};
|
|
264
389
|
this.once(name, onEvent);
|
|
265
390
|
});
|
|
@@ -398,8 +523,15 @@ export class DapClient extends EventEmitter {
|
|
|
398
523
|
* before returning — see `dbg_launch`'s `stop_on_entry`. Godot 4.7 does not honour
|
|
399
524
|
* `stopOnEntry` at all, so this wait times out there and the caller is told so
|
|
400
525
|
* rather than being handed a bare `running` that reads like it worked.
|
|
526
|
+
*
|
|
527
|
+
* 🆕 `initializedSeen` (267) is the SAME DISCIPLINE applied to the wait one line up.
|
|
528
|
+
* The `initialized` wait had been resolving on its own timer since this handshake was
|
|
529
|
+
* written, so a five-second silence and a prompt event were the same observable and the
|
|
530
|
+
* caller was handed a session that read exactly like a conformant one. It is reported,
|
|
531
|
+
* not refused: every adapter that works today keeps working, and the four launch tools
|
|
532
|
+
* say which of the two happened — the shape `stop_on_entry_honored` already ships in.
|
|
401
533
|
*/
|
|
402
|
-
async start(mode, args, entryStopWaitMs = 0) {
|
|
534
|
+
async start(mode, args, entryStopWaitMs = 0, requireInitialized = true) {
|
|
403
535
|
// Remember how we started so restart() can reuse (or override) these params.
|
|
404
536
|
this.lastStartMode = mode;
|
|
405
537
|
this.lastStartArgs = args;
|
|
@@ -407,8 +539,21 @@ export class DapClient extends EventEmitter {
|
|
|
407
539
|
// A fresh session re-detects against whatever adapter answers this time — a
|
|
408
540
|
// restart may reach a different build than the one that dropped last time.
|
|
409
541
|
this.droppedModifiers.clear();
|
|
410
|
-
// Listen for `initialized` before we ask, so we cannot miss it.
|
|
411
|
-
|
|
542
|
+
// Listen for `initialized` before we ask, so we cannot miss it. The window is
|
|
543
|
+
// captured rather than recomputed at the report site, so a warning or refusal names
|
|
544
|
+
// the wait that actually happened rather than the one the code would compute later.
|
|
545
|
+
//
|
|
546
|
+
// 🔴 TWO WINDOWS, AND WHICH ONE APPLIES IS THE WHOLE OF 268's DECISION. Refusing is
|
|
547
|
+
// the default now, so the ceiling that made a longer wait pointless has gone with it:
|
|
548
|
+
// when nothing is going to proceed regardless, the honest bound is the deadline the
|
|
549
|
+
// CALLER declared. A conformant adapter that takes six seconds used to be configured
|
|
550
|
+
// out of order at five and told nobody; it now simply succeeds. The five-second
|
|
551
|
+
// ceiling survives only on the opt-out path, where it must, because that path exists
|
|
552
|
+
// to reproduce 267's behaviour exactly — including the number its warning prints.
|
|
553
|
+
const initializedWaitMs = requireInitialized
|
|
554
|
+
? this.timeoutMs
|
|
555
|
+
: Math.min(this.timeoutMs, INITIALIZED_WAIT_CEILING_MS);
|
|
556
|
+
const onInit = this.waitEvent("initialized", initializedWaitMs);
|
|
412
557
|
this.capabilities = await this.request("initialize", {
|
|
413
558
|
clientID: "breakpoint-mcp",
|
|
414
559
|
clientName: "Godot Breakpoint MCP",
|
|
@@ -436,7 +581,21 @@ export class DapClient extends EventEmitter {
|
|
|
436
581
|
});
|
|
437
582
|
// An entry stop can arrive before configurationDone answers — arm first.
|
|
438
583
|
const entryStop = entryStopWaitMs > 0 ? this.waitEvent("stopped", entryStopWaitMs) : null;
|
|
439
|
-
await onInit
|
|
584
|
+
// 🔴 CAPTURED, NOT DISCARDED (267). `await onInit` threw this away for as long as the
|
|
585
|
+
// handshake has existed, which is why `setBreakpoints` going out ahead of the event
|
|
586
|
+
// that licenses it was invisible: the failing case and the succeeding case returned
|
|
587
|
+
// the same `undefined`.
|
|
588
|
+
const initializedSeen = await onInit;
|
|
589
|
+
// 🔴 THE REFUSAL GOES HERE AND NOWHERE LATER (268), because the next line is the
|
|
590
|
+
// defect. `applyAllBreakpoints()` is `setBreakpoints` going out ahead of the event
|
|
591
|
+
// the DAP specification says must precede it — the thing 266 measured against a stub
|
|
592
|
+
// adapter and 267 made observable without stopping. Refusing after it would leave the
|
|
593
|
+
// adapter holding breakpoints from a session the caller was told did not start.
|
|
594
|
+
if (!initializedSeen && requireInitialized) {
|
|
595
|
+
this.state = "terminated";
|
|
596
|
+
this.sessionStarted = false;
|
|
597
|
+
throw new DapError(mode, unannouncedMessage("The debug adapter", mode, initializedWaitMs), unannouncedRemedy(DAP_PEER, DAP_TIMEOUT_KNOB), DAP_UNANNOUNCED_CODE);
|
|
598
|
+
}
|
|
440
599
|
await this.applyAllBreakpoints();
|
|
441
600
|
await this.request("configurationDone", {}).catch(() => undefined);
|
|
442
601
|
// 🔴 By the time we get here an already-arrived rejection has ALREADY run its
|
|
@@ -460,7 +619,7 @@ export class DapClient extends EventEmitter {
|
|
|
460
619
|
if (startFailure)
|
|
461
620
|
throw startFailure;
|
|
462
621
|
}
|
|
463
|
-
return { entryStopSeen: this.stoppedNow() };
|
|
622
|
+
return { entryStopSeen: this.stoppedNow(), initializedSeen, initializedWaitMs };
|
|
464
623
|
}
|
|
465
624
|
threadId() {
|
|
466
625
|
return this.lastStoppedThreadId ?? 1;
|
|
@@ -532,7 +691,7 @@ export class DapClient extends EventEmitter {
|
|
|
532
691
|
*/
|
|
533
692
|
async restart(overrideArgs = {}, waitMs = 15000) {
|
|
534
693
|
if (!this.lastStartMode || !this.lastStartArgs) {
|
|
535
|
-
throw new DapError("restart", "no debug session to restart
|
|
694
|
+
throw new DapError("restart", "no debug session to restart", DAP_RESTART_REMEDY);
|
|
536
695
|
}
|
|
537
696
|
const args = { ...this.lastStartArgs, ...overrideArgs };
|
|
538
697
|
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.80.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,66 @@
|
|
|
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
|
+
/**
|
|
49
|
+
* The next action when an adapter answered `initialize` and never announced itself.
|
|
50
|
+
*
|
|
51
|
+
* 🔴 IT NAMES BOTH WAYS OUT, BECAUSE THE HOST CANNOT TELL THEM APART FROM HERE. Either
|
|
52
|
+
* the adapter is slower than the caller's own declared deadline — raise it — or it does
|
|
53
|
+
* not implement the `initialized` event at all, in which case a session can still be had
|
|
54
|
+
* by opting out and accepting that breakpoints are applied ahead of the event that
|
|
55
|
+
* licenses them, which is what every session did silently before 268. Naming only the
|
|
56
|
+
* knob would send an operator to lengthen a wait that is never going to end.
|
|
57
|
+
*
|
|
58
|
+
* Written to check 28's grammar: an imperative at the head, a full stop at the end, and
|
|
59
|
+
* nothing named that does not resolve.
|
|
60
|
+
*/
|
|
61
|
+
export function unannouncedRemedy(peer, knob) {
|
|
62
|
+
return (`Raise \`${knob}\` if ${peer} is merely slow to announce itself, or set ` +
|
|
63
|
+
`\`GODOT_DAP_REQUIRE_INITIALIZED=0\` to start anyway against an adapter that never ` +
|
|
64
|
+
`sends \`initialized\`.`);
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=timeout-cause.js.map
|
package/dist/timeout-caveat.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { annotationsFor } from "./annotations.js";
|
|
2
|
+
import { BRIDGE_TIMEOUT_CODE, bridgeErrorLabel } from "./bridge.js";
|
|
2
3
|
/**
|
|
3
4
|
* The marker identifying a bridge timeout envelope, built by
|
|
4
5
|
* `tools/editor/common.ts`'s `fail()`. Deliberately narrow: LSP and DAP timeouts
|
|
5
|
-
* are a different transport with a different failure mode
|
|
6
|
-
* mutation is implied
|
|
7
|
-
*
|
|
6
|
+
* are a different transport with a different failure mode — no editor-side
|
|
7
|
+
* mutation is implied — and the two DAP planes classify their own by
|
|
8
|
+
* `DapError.code`, which this neither reads nor disturbs.
|
|
9
|
+
*
|
|
10
|
+
* 🔴 **DERIVED SINCE 268; A HAND-COPIED LITERAL BEFORE IT.** This constant used to
|
|
11
|
+
* spell `Bridge error [timeout]` out, here, while the template producing it lives in
|
|
12
|
+
* `tools/editor/common.ts` and the code it embeds is raised in `bridge.ts`. Three
|
|
13
|
+
* files, one sentence, nothing joining them — so a reword of `fail()`'s label would
|
|
14
|
+
* have left this matching nothing, and the caveat that tells a caller their timed-out
|
|
15
|
+
* `node_add` may ALREADY have landed would have gone quiet on every mutating tool
|
|
16
|
+
* with the whole suite still green. `dap-timeout-predicate-reads-prose` with a hundred
|
|
17
|
+
* times the blast radius, found only by asking what ELSE in the tree decides behaviour
|
|
18
|
+
* by matching English.
|
|
8
19
|
*/
|
|
9
|
-
const BRIDGE_TIMEOUT_MARK =
|
|
20
|
+
const BRIDGE_TIMEOUT_MARK = bridgeErrorLabel(BRIDGE_TIMEOUT_CODE);
|
|
10
21
|
/** Mutating, but repeating it lands in the same state — retrying is safe. */
|
|
11
22
|
export const CAVEAT_IDEMPOTENT = "The request WAS delivered to the editor and may already have been applied. " +
|
|
12
23
|
"This tool is idempotent, so retrying is safe.";
|