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 CHANGED
@@ -65,6 +65,68 @@ export function remedyClause(err) {
65
65
  const r = err?.remedy;
66
66
  return typeof r === "string" && r !== "" ? ` โ€” ${r}` : "";
67
67
  }
68
+ /** The code this class raises when a bridge request outlives its own deadline. */
69
+ export const BRIDGE_TIMEOUT_CODE = "timeout";
70
+ /**
71
+ * The label `tools/editor/common.ts`'s `fail()` puts in front of a bridge failure.
72
+ *
73
+ * ๐Ÿ”ด **ONE BUILDER, TWO READERS, AND THE SECOND ONE WAS A STRING LITERAL UNTIL 268.**
74
+ * `timeout-caveat.ts` decides whether a mutating tool gets its retry caveat by
75
+ * looking for `Bridge error [timeout]` inside the rendered envelope โ€” a substring it
76
+ * spelled out itself, one file away from the template that produces it. That is
77
+ * `dap-timeout-predicate-reads-prose` again with a far wider blast radius: a reword of
78
+ * `fail()` would silently stop every non-idempotent tool from warning that a timed-out
79
+ * mutation may already have landed, and nothing compares the two spellings. Derived here
80
+ * so the two move together, and check 32 refuses a fresh literal.
81
+ */
82
+ export function bridgeErrorLabel(code) {
83
+ return `Bridge error [${code}]`;
84
+ }
85
+ /**
86
+ * The code raised when the request could not be handed to the socket at all.
87
+ *
88
+ * ๐Ÿ”ด **ONE WORD, TWO PRODUCERS, AND ONLY ONE OF THEM HAS A REMEDY THAT FITS (268).** The
89
+ * ADDON raises `write_failed` too, from `operations.gd`, for a FILE it could not open for
90
+ * writing โ€” and `error_remedies.gd` answers it with *"Check the target path is inside the
91
+ * project and not read-only; the number is a Godot Error code."* That sentence is correct
92
+ * for the addon and nonsense for this site, which has no path, no file and no Godot error
93
+ * code: it is a TCP socket that went away. The collision was invisible while this branch
94
+ * was believed unreachable; 268 measured it reachable 200/200 and it became live.
95
+ *
96
+ * The CODE is kept and the message and remedy are made to fit โ€” 262's disposition for an
97
+ * overloaded shipped code, for the same reason: fix what the reader acts on, and leave a
98
+ * wire vocabulary that callers may already branch on alone. Whether the two producers
99
+ * should share a word at all is a wire question, and it is enqueued as one rather than
100
+ * decided here.
101
+ */
102
+ export const BRIDGE_WRITE_FAILED = "write_failed";
103
+ /**
104
+ * The next action for a request that never reached the socket.
105
+ *
106
+ * ๐Ÿ”ด IT IS THE ONE FAILURE IN THIS CLASS THAT CAN SAY *RETRY* WITHOUT QUALIFICATION, and
107
+ * that is the whole value of separating it. `timeout-caveat.ts` exists because a bridge
108
+ * TIMEOUT may already have been applied by the editor; `closeRemedy` cannot promise
109
+ * otherwise either, since a request in flight when the socket dropped may have landed.
110
+ * This one is different in a way the host actually knows: the frame was never handed to
111
+ * the kernel, so nothing can have been applied, so a retry is a first attempt and not a
112
+ * second. 264's rule โ€” measure N families, speak for N โ€” read in the direction that
113
+ * licenses a stronger sentence rather than a weaker one.
114
+ */
115
+ export const WRITE_FAILED_REMEDY = "Retry the call โ€” the request was never sent, so nothing was applied and this is a " +
116
+ "first attempt rather than a second. A recurrence means the host is tearing the " +
117
+ "connection down mid-request.";
118
+ /**
119
+ * The sentence for a request that never reached the socket.
120
+ *
121
+ * `cause` is node's own error, kept in a parenthetical rather than used as the whole
122
+ * message: `Cannot call write after a stream was destroyed` is a true statement about a
123
+ * Node stream and tells a caller of `node_add` nothing about their call, which is what
124
+ * this site shipped until 268.
125
+ */
126
+ export function writeFailedMessage(method, cause) {
127
+ return (`Bridge request '${method}' was never sent โ€” the connection was torn down between ` +
128
+ `preparing the request and writing it, so the editor did not receive it (${cause.message}).`);
129
+ }
68
130
  /**
69
131
  * TCP client for the in-editor Breakpoint MCP addon. Speaks newline-delimited
70
132
  * JSON. Requests are correlated to responses by `id`. Connects lazily and
@@ -396,9 +458,15 @@ export class BridgeClient {
396
458
  const timer = setTimeout(() => {
397
459
  this.pending.delete(id);
398
460
  // Remember the id BEFORE rejecting, so a reply already in flight is
399
- // reconciled rather than dropped as anonymous. The message keeps the
400
- // exact `timed out after <n>ms` phrasing โ€” tools/dap.ts:29 and
401
- // tools/csdap.ts:31 branch on that substring.
461
+ // reconciled rather than dropped as anonymous.
462
+ //
463
+ // ๐Ÿ”ด THIS COMMENT USED TO SAY THE `timed out after <n>ms` PHRASING WAS
464
+ // LOAD-BEARING, NAMING `tools/dap.ts:29` AND `tools/csdap.ts:31`. Both line
465
+ // numbers had drifted by more than a hundred lines, and as of 268 the claim is
466
+ // false as well: those predicates read `DapError.code`, and this class's own
467
+ // caveat reader derives its marker from `bridgeErrorLabel`. A comment naming a
468
+ // line number is a claim that goes stale in silence โ€” which is the whole reason
469
+ // the join is structural now.
402
470
  this.ledger.note(id, method, timeoutMs);
403
471
  // ๐Ÿ”ด 262 ยง2 CALLED THIS "THE ONE HOST-RAISED FAILURE THAT HAS A KNOWABLE CAUSE",
404
472
  // and said everything else this class raises โ€” "a closed socket, a peer that never
@@ -408,14 +476,30 @@ export class BridgeClient {
408
476
  // line โ€” a peer that never answered really does leave nothing behind โ€” which is
409
477
  // why the one cause the host can supply comes from outside the socket: its own
410
478
  // debugger holding the game. See `setHoldProbe`, and 264's census for the rest.
411
- reject(new BridgeError("timeout", `Bridge request '${method}' timed out after ${timeoutMs}ms`, this.holdProbe?.() ?? undefined));
479
+ reject(new BridgeError(BRIDGE_TIMEOUT_CODE, `Bridge request '${method}' timed out after ${timeoutMs}ms`, this.holdProbe?.() ?? undefined));
412
480
  }, timeoutMs);
413
481
  this.pending.set(id, { resolve: resolve, reject, timer, method });
414
482
  socket.write(payload, (err) => {
415
483
  if (err) {
416
484
  clearTimeout(timer);
417
485
  this.pending.delete(id);
418
- reject(new BridgeError("write_failed", err.message));
486
+ // ๐Ÿ”ด REACHABLE, AND 265 CONCLUDED THE OPPOSITE FROM THE SAME TECHNIQUE (268).
487
+ // 265 broke a live socket four ways and read `bridge_closed` every time, and
488
+ // reasoned that `onClose()` always rejects the pending map first. Measured here
489
+ // 300/300 at the node level, that ordering is BACKWARDS: on a destroyed socket
490
+ // the write callback fires with ERR_STREAM_DESTROYED **before** the `close`
491
+ // event, so this line wins whenever the socket dies inside the await gap
492
+ // `request()` leaves between `connect()` and here. Driven through the real
493
+ // client โ€” `request()` not awaited, `close()` on the next statement โ€” the caller
494
+ // received THIS rejection 200 times out of 200. 265's drives all broke the
495
+ // socket and then WAITED, which is the one window in which they could not win.
496
+ //
497
+ // The sentence is worth having because this case knows something the other two
498
+ // do not: the payload never left. A `timeout` may have been applied by the
499
+ // editor and `bridge_closed` may have been too, so neither can tell a caller to
500
+ // retry; this one can, and 267's rule is that a next action rides in the field
501
+ // rather than inside the message.
502
+ reject(new BridgeError(BRIDGE_WRITE_FAILED, writeFailedMessage(method, err), WRITE_FAILED_REMEDY));
419
503
  }
420
504
  });
421
505
  });
package/dist/config.js CHANGED
@@ -111,6 +111,18 @@ function positiveInt(raw, fallback) {
111
111
  const n = Number(t);
112
112
  return Number.isSafeInteger(n) && n > 0 && n <= 2_147_483_647 ? n : fallback;
113
113
  }
114
+ /**
115
+ * An opt-OUT flag: true unless the operator spelled a recognised falsehood.
116
+ *
117
+ * ๐Ÿ”ด THE UNRECOGNISED VALUE KEEPS THE SAFE DEFAULT, AND THAT IS THE OPPOSITE OF
118
+ * `positiveInt`'s reasoning ON PURPOSE. A malformed deadline falls back because both
119
+ * outcomes are ordinary; a malformed *guard* setting must not disable the guard, or a
120
+ * typo โ€” `GODOT_DAP_REQUIRE_INITIALIZED=no thanks` โ€” turns a refusal off silently. Only
121
+ * `0`, `false`, `off` and `no` are read as intent, case-insensitively.
122
+ */
123
+ function optOut(raw) {
124
+ return !/^(0|false|off|no)$/i.test((raw ?? "").trim());
125
+ }
114
126
  export function loadConfig() {
115
127
  const projectPath = process.env.GODOT_PROJECT ?? process.cwd();
116
128
  // The C# project defaults to the main project, but is usually pointed at a
@@ -136,6 +148,7 @@ export function loadConfig() {
136
148
  dapTimeoutMs: positiveInt(process.env.GODOT_DAP_TIMEOUT_MS, 20000),
137
149
  dapSetVarTimeoutMs: positiveInt(process.env.GODOT_DAP_SETVAR_TIMEOUT_MS, 8000),
138
150
  dapEvaluateTimeoutMs: positiveInt(process.env.GODOT_DAP_EVALUATE_TIMEOUT_MS, 8000),
151
+ dapRequireInitialized: optOut(process.env.GODOT_DAP_REQUIRE_INITIALIZED),
139
152
  csDapCmd: process.env.GODOT_CSDAP_CMD ?? "netcoredbg",
140
153
  csDapArgs: (process.env.GODOT_CSDAP_ARGS ?? "--interpreter=vscode").split(/\s+/).filter(Boolean),
141
154
  // The default program cs_dbg_launch launches is the Mono/.NET Godot binary. GODOT_CSHARP_BIN
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, DAP_TIMEOUT_CODE, DAP_UNANNOUNCED_CODE, INITIALIZED_WAIT_CEILING_MS, unannouncedMessage, } from "./dap.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 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, "GODOT_CSDAP_TIMEOUT_MS");
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 SAME ERRNO SPLIT `bridge.ts` GOT (264 ยง3), IN THE MESSAGE BECAUSE THIS CLASS
132
- // HAS NOWHERE ELSE TO PUT IT. `DapError` carries no `remedy` field, and the plane's
133
- // `fail()` renders no `remedyClause`, so the next action goes where the caller will
134
- // actually read it. 264's census records the asymmetry rather than hiding it: of 25
135
- // host-raised failures about the world, 13 are on classes that cannot carry an answer.
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}${remedy ? ` โ€” ${remedy}` : ""}`));
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
- reject(new DapError(command, `C# DAP '${command}' timed out after ${timeoutMs}ms`));
233
+ // ๐Ÿ”ด THE WORDING IS NO LONGER LOAD-BEARING (268) โ€” see `dap.ts`'s twin. The code
234
+ // is what `tools/csdap.ts`'s `isDapTimeout` reads; the sentence is free.
235
+ reject(new DapError(command, `C# DAP '${command}' timed out after ${timeoutMs}ms`, timeoutRemedy(CSDAP_PEER, CSDAP_TIMEOUT_KNOB), DAP_TIMEOUT_CODE));
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
  });
@@ -327,12 +350,18 @@ export class CsDapClient extends EventEmitter {
327
350
  * afterwards failed with a bare hex code against a phantom session. The same held
328
351
  * for `program: ""` and for `cs_dbg_attach` on a process id that does not exist.
329
352
  */
330
- async start(mode, args) {
353
+ async start(mode, args, requireInitialized = true) {
331
354
  this.lastStartMode = mode;
332
355
  this.lastStartArgs = args;
333
356
  this.sessionStarted = false;
334
- // Listen for `initialized` before we ask, so we cannot miss it.
335
- const onInit = this.waitEvent("initialized", Math.min(this.timeoutMs, 5000));
357
+ // Listen for `initialized` before we ask, so we cannot miss it. Two windows since
358
+ // 268 โ€” see `dap.ts`'s twin for the argument. Refusing is the default, so the wait
359
+ // runs to the caller's own deadline; the five-second ceiling survives on the opt-out
360
+ // path alone, which exists to reproduce 267's behaviour including its numbers.
361
+ const initializedWaitMs = requireInitialized
362
+ ? this.timeoutMs
363
+ : Math.min(this.timeoutMs, INITIALIZED_WAIT_CEILING_MS);
364
+ const onInit = this.waitEvent("initialized", initializedWaitMs);
336
365
  this.capabilities = await this.request("initialize", {
337
366
  clientID: "breakpoint-mcp",
338
367
  clientName: "Godot Breakpoint MCP",
@@ -364,7 +393,16 @@ export class CsDapClient extends EventEmitter {
364
393
  // EventEmitter and throws when unlistened. This one is safe to ignore.
365
394
  this.emit("start_failed", err);
366
395
  });
367
- await onInit;
396
+ // ๐Ÿ”ด CAPTURED, NOT DISCARDED (267) โ€” see `dap.ts`'s twin.
397
+ const initializedSeen = await onInit;
398
+ // ๐Ÿ”ด REFUSED, NOT REPORTED (268) โ€” and BEFORE `applyAllBreakpoints()`, because that
399
+ // call is the defect itself. See `dap.ts`'s twin.
400
+ if (!initializedSeen && requireInitialized) {
401
+ this.state = "terminated";
402
+ this.sessionStarted = false;
403
+ this.configured = false;
404
+ throw new DapError(mode, unannouncedMessage("The C# debug adapter", mode, initializedWaitMs), unannouncedRemedy(CSDAP_PEER, CSDAP_TIMEOUT_KNOB), DAP_UNANNOUNCED_CODE);
405
+ }
368
406
  await this.applyAllBreakpoints();
369
407
  // Arm the entry-stop wait BEFORE configurationDone is sent: netcoredbg can emit
370
408
  // `stopped` before its configurationDone response lands, and a listener attached
@@ -389,9 +427,13 @@ export class CsDapClient extends EventEmitter {
389
427
  this.state = "terminated";
390
428
  void startReq;
391
429
  const detail = fatal?.message ?? String(fatal);
430
+ // ๐Ÿ”ด THE NEXT ACTION MOVED OUT OF THE MESSAGE (267). It read *check that the program
431
+ // exists and is a .NET assembly* mid-sentence, between a state report and an
432
+ // argument dump โ€” 264's census counted exactly this shape among the SEVEN that
433
+ // named an action nothing structural could read. The message now reports state; the
434
+ // field carries the instruction, and check 28's grammar reader can see it.
392
435
  throw new DapError(mode, `the C# debug adapter did not start the session: ${detail}. No debug session is ` +
393
- `running โ€” check that the program exists and is a .NET assembly (${mode} args: ` +
394
- `${JSON.stringify(args).slice(0, 200)}).`);
436
+ `running (${mode} args: ${JSON.stringify(args).slice(0, 200)}).`, CS_START_REMEDY);
395
437
  }
396
438
  this.configured = true;
397
439
  this.sessionStarted = true;
@@ -411,6 +453,7 @@ export class CsDapClient extends EventEmitter {
411
453
  // simply reports `running`, as before.
412
454
  if (entryStop)
413
455
  await entryStop;
456
+ return { initializedSeen, initializedWaitMs };
414
457
  }
415
458
  threadId() {
416
459
  return this.lastStoppedThreadId ?? 1;
@@ -482,7 +525,7 @@ export class CsDapClient extends EventEmitter {
482
525
  */
483
526
  async restart(overrideArgs = {}, waitMs = 15000) {
484
527
  if (!this.lastStartMode || !this.lastStartArgs) {
485
- throw new DapError("restart", "no C# debug session to restart โ€” call cs_dbg_launch or cs_dbg_attach first");
528
+ throw new DapError("restart", "no C# debug session to restart", CS_RESTART_REMEDY);
486
529
  }
487
530
  const args = { ...this.lastStartArgs, ...overrideArgs };
488
531
  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, "GODOT_CSLSP_TIMEOUT_MS");
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), IN THE MESSAGE BECAUSE THIS CLASS
143
- // HAS NOWHERE ELSE TO PUT IT. `LspError` carries no `remedy` field, and the plane's
144
- // `fail()` renders no `remedyClause`, so the next action goes where the caller will
145
- // actually read it. 264's census records the asymmetry rather than hiding it: of 25
146
- // host-raised failures about the world, 13 are on classes that cannot carry an answer.
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}${remedy ? ` โ€” ${remedy}` : ""}`));
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) => {