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/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, TWO READERS (264). The ledger names this peer when a reply arrives
9
- * late and `closeRemedy` names it when the connection drops. Two literals would let the
10
- * same peer acquire two names on the same run, which is the class of drift 257's
11
- * per-instance deadline noun was written to stop.
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
- constructor(command, message) {
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, "GODOT_DAP_TIMEOUT_MS");
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), IN THE MESSAGE BECAUSE THIS CLASS
156
- // HAS NOWHERE ELSE TO PUT IT. `DapError` carries no `remedy` field, and the plane's
157
- // `fail()` renders no `remedyClause`, so the next action goes where the caller will
158
- // actually read it. 264's census records the asymmetry rather than hiding it: of 25
159
- // host-raised failures about the world, 13 are on classes that cannot carry an answer.
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}${remedy ? ` — ${remedy}` : ""}`));
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
- reject(new DapError(command, `DAP '${command}' timed out after ${timeoutMs}ms`));
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
- const onInit = this.waitEvent("initialized", Math.min(this.timeoutMs, 5000));
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 — call dbg_launch or dbg_attach first");
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;
@@ -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
- * 🔵 WHAT THIS DELIBERATELY DOES NOT DO. `godot_process_output` still answers
46
- * `exit_code: null` for a signal-killed child and carries no `signal` key: that is a wire
47
- * addition to a shipped tool, it would make this cut a MINOR, and it is enqueued rather
48
- * than folded in. The capture is internal; only the two peer sentences read it today.
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 (`Read the output quoted above for the peer(s) that exited and fix what stopped them, ` +
88
- `then check the Breakpoint MCP plugin is enabled for the one(s) that stayed up and never ` +
89
- `answered — two different things failed here and only the second is about the addon.`);
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.78.3" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
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
- constructor(code, message) {
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, "GODOT_LSP_TIMEOUT_MS");
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), IN THE MESSAGE BECAUSE THIS CLASS
147
- // HAS NOWHERE ELSE TO PUT IT. `LspError` carries no `remedy` field, and the plane's
148
- // `fail()` renders no `remedyClause`, so the next action goes where the caller will
149
- // actually read it. 264's census records the asymmetry rather than hiding it: of 25
150
- // host-raised failures about the world, 13 are on classes that cannot carry an answer.
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}${remedy ? ` — ${remedy}` : ""}`));
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 (`The process launched and no wait was requested (wait_timeout_ms 0), so nothing here knows whether the ` +
100
- `runtime bridge at ${cfg.runtimeHost}:${cfg.runtimePort} is up yet. Call runtime_get_tree to find out, or ` +
101
- `re-run without wait_timeout_ms and this tool will wait and answer.`);
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 (`The process launched but the runtime bridge at ${cfg.runtimeHost}:${cfg.runtimePort} did not answer ping ` +
104
- `within ${waitedMs} ms, so a runtime_* call will fail until it does. Is the "Breakpoint MCP" plugin enabled in ` +
105
- `this project? The runtime autoload it registers is what binds the port. If the project is simply slow to boot, ` +
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
@@ -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 (no editor-side
6
- * mutation is implied), and `tools/dap.ts:29` / `tools/csdap.ts:31` branch on
7
- * their own `/timed out after/` predicate, which this must not disturb.
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 = "Bridge error [timeout]";
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.";