@animalabs/agent-framework 0.33.0 → 0.34.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.
Files changed (176) hide show
  1. package/README.md +136 -8
  2. package/dist/src/agent.d.ts +25 -4
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +68 -13
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/append-all.d.ts +5 -0
  7. package/dist/src/append-all.d.ts.map +1 -0
  8. package/dist/src/append-all.js +8 -0
  9. package/dist/src/append-all.js.map +1 -0
  10. package/dist/src/context-options.d.ts +9 -6
  11. package/dist/src/context-options.d.ts.map +1 -1
  12. package/dist/src/context-options.js +13 -8
  13. package/dist/src/context-options.js.map +1 -1
  14. package/dist/src/cut-off-round.js +4 -4
  15. package/dist/src/dendrite/registry.d.ts.map +1 -1
  16. package/dist/src/dendrite/registry.js +4 -2
  17. package/dist/src/dendrite/registry.js.map +1 -1
  18. package/dist/src/failure-text.d.ts +36 -0
  19. package/dist/src/failure-text.d.ts.map +1 -0
  20. package/dist/src/failure-text.js +89 -0
  21. package/dist/src/failure-text.js.map +1 -0
  22. package/dist/src/framework.d.ts +94 -30
  23. package/dist/src/framework.d.ts.map +1 -1
  24. package/dist/src/framework.js +987 -284
  25. package/dist/src/framework.js.map +1 -1
  26. package/dist/src/gate/event-gate.d.ts +40 -6
  27. package/dist/src/gate/event-gate.d.ts.map +1 -1
  28. package/dist/src/gate/event-gate.js +66 -25
  29. package/dist/src/gate/event-gate.js.map +1 -1
  30. package/dist/src/gate/reply-judge.d.ts +4 -0
  31. package/dist/src/gate/reply-judge.d.ts.map +1 -1
  32. package/dist/src/gate/reply-judge.js +12 -3
  33. package/dist/src/gate/reply-judge.js.map +1 -1
  34. package/dist/src/gate/suppression-review.js +2 -2
  35. package/dist/src/gate/suppression-review.js.map +1 -1
  36. package/dist/src/gate/types.d.ts +7 -0
  37. package/dist/src/gate/types.d.ts.map +1 -1
  38. package/dist/src/heartbeat/heartbeat.js +1 -1
  39. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  40. package/dist/src/index.d.ts +1 -0
  41. package/dist/src/index.d.ts.map +1 -1
  42. package/dist/src/index.js +1 -0
  43. package/dist/src/index.js.map +1 -1
  44. package/dist/src/inference-bounds.d.ts +2 -1
  45. package/dist/src/inference-bounds.d.ts.map +1 -1
  46. package/dist/src/inference-bounds.js +9 -6
  47. package/dist/src/inference-bounds.js.map +1 -1
  48. package/dist/src/mcpl/channel-registry.d.ts +57 -13
  49. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  50. package/dist/src/mcpl/channel-registry.js +136 -24
  51. package/dist/src/mcpl/channel-registry.js.map +1 -1
  52. package/dist/src/mcpl/modern-connection.d.ts +109 -15
  53. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  54. package/dist/src/mcpl/modern-connection.js +593 -86
  55. package/dist/src/mcpl/modern-connection.js.map +1 -1
  56. package/dist/src/mcpl/protocol-family.d.ts +3 -1
  57. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  58. package/dist/src/mcpl/protocol-family.js +15 -9
  59. package/dist/src/mcpl/protocol-family.js.map +1 -1
  60. package/dist/src/mcpl/server-connection.d.ts +37 -18
  61. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  62. package/dist/src/mcpl/server-connection.js +31 -16
  63. package/dist/src/mcpl/server-connection.js.map +1 -1
  64. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  65. package/dist/src/mcpl/tool-presence.js +10 -6
  66. package/dist/src/mcpl/tool-presence.js.map +1 -1
  67. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  68. package/dist/src/mcpl/toolset-host.js +7 -6
  69. package/dist/src/mcpl/toolset-host.js.map +1 -1
  70. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  71. package/dist/src/mcpl/toolsets-module.js +2 -1
  72. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  73. package/dist/src/mcpl/types.d.ts +25 -4
  74. package/dist/src/mcpl/types.d.ts.map +1 -1
  75. package/dist/src/mcpl/types.js.map +1 -1
  76. package/dist/src/modules/discord/index.js +1 -1
  77. package/dist/src/modules/discord/index.js.map +1 -1
  78. package/dist/src/modules/health/index.js +2 -2
  79. package/dist/src/modules/health/index.js.map +1 -1
  80. package/dist/src/modules/history/semantic.js +3 -3
  81. package/dist/src/modules/history/semantic.js.map +1 -1
  82. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  83. package/dist/src/modules/workspace/index.js +4 -3
  84. package/dist/src/modules/workspace/index.js.map +1 -1
  85. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  86. package/dist/src/modules/workspace/observe.js +2 -1
  87. package/dist/src/modules/workspace/observe.js.map +1 -1
  88. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  89. package/dist/src/modules/workspace/reconcile.js +3 -2
  90. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  91. package/dist/src/notice-tags.d.ts +10 -8
  92. package/dist/src/notice-tags.d.ts.map +1 -1
  93. package/dist/src/notice-tags.js +10 -8
  94. package/dist/src/notice-tags.js.map +1 -1
  95. package/dist/src/presentation.d.ts +2 -1
  96. package/dist/src/presentation.d.ts.map +1 -1
  97. package/dist/src/presentation.js +4 -2
  98. package/dist/src/presentation.js.map +1 -1
  99. package/dist/src/reasoning-loss.d.ts +5 -0
  100. package/dist/src/reasoning-loss.d.ts.map +1 -0
  101. package/dist/src/reasoning-loss.js +8 -0
  102. package/dist/src/reasoning-loss.js.map +1 -0
  103. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  104. package/dist/src/recovery/offline-branch.js +5 -4
  105. package/dist/src/recovery/offline-branch.js.map +1 -1
  106. package/dist/src/recovery/recover-cli.js +2 -1
  107. package/dist/src/recovery/recover-cli.js.map +1 -1
  108. package/dist/src/sent-rows.js +1 -1
  109. package/dist/src/supplied-text.d.ts +1 -1
  110. package/dist/src/supplied-text.js +3 -3
  111. package/dist/src/supplied-text.js.map +1 -1
  112. package/dist/src/thinking-drops.d.ts +2 -0
  113. package/dist/src/thinking-drops.d.ts.map +1 -1
  114. package/dist/src/thinking-drops.js.map +1 -1
  115. package/dist/src/tool-presentation.d.ts.map +1 -1
  116. package/dist/src/tool-presentation.js +38 -9
  117. package/dist/src/tool-presentation.js.map +1 -1
  118. package/dist/src/tune-out/coordinator.d.ts +13 -4
  119. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  120. package/dist/src/tune-out/coordinator.js +33 -16
  121. package/dist/src/tune-out/coordinator.js.map +1 -1
  122. package/dist/src/tune-out/tools.d.ts.map +1 -1
  123. package/dist/src/tune-out/tools.js +2 -1
  124. package/dist/src/tune-out/tools.js.map +1 -1
  125. package/dist/src/types/agent.d.ts +39 -5
  126. package/dist/src/types/agent.d.ts.map +1 -1
  127. package/dist/src/types/framework.d.ts +43 -8
  128. package/dist/src/types/framework.d.ts.map +1 -1
  129. package/dist/src/types/inference-log.d.ts +1 -0
  130. package/dist/src/types/inference-log.d.ts.map +1 -1
  131. package/dist/src/types/trace.d.ts +64 -4
  132. package/dist/src/types/trace.d.ts.map +1 -1
  133. package/package.json +3 -3
  134. package/src/agent.ts +71 -14
  135. package/src/append-all.ts +6 -0
  136. package/src/context-options.ts +18 -8
  137. package/src/cut-off-round.ts +4 -4
  138. package/src/dendrite/registry.ts +4 -2
  139. package/src/failure-text.ts +88 -0
  140. package/src/framework.ts +1018 -293
  141. package/src/gate/event-gate.ts +90 -26
  142. package/src/gate/reply-judge.ts +12 -3
  143. package/src/gate/suppression-review.ts +2 -2
  144. package/src/gate/types.ts +7 -0
  145. package/src/heartbeat/heartbeat.ts +1 -1
  146. package/src/index.ts +1 -0
  147. package/src/inference-bounds.ts +11 -6
  148. package/src/mcpl/channel-registry.ts +160 -31
  149. package/src/mcpl/modern-connection.ts +629 -91
  150. package/src/mcpl/protocol-family.ts +15 -9
  151. package/src/mcpl/server-connection.ts +28 -15
  152. package/src/mcpl/tool-presence.ts +10 -5
  153. package/src/mcpl/toolset-host.ts +7 -6
  154. package/src/mcpl/toolsets-module.ts +2 -1
  155. package/src/mcpl/types.ts +25 -4
  156. package/src/modules/discord/index.ts +1 -1
  157. package/src/modules/health/index.ts +2 -2
  158. package/src/modules/history/semantic.ts +3 -3
  159. package/src/modules/workspace/index.ts +4 -3
  160. package/src/modules/workspace/observe.ts +2 -1
  161. package/src/modules/workspace/reconcile.ts +3 -2
  162. package/src/notice-tags.ts +11 -8
  163. package/src/presentation.ts +4 -2
  164. package/src/reasoning-loss.ts +7 -0
  165. package/src/recovery/offline-branch.ts +5 -4
  166. package/src/recovery/recover-cli.ts +2 -1
  167. package/src/sent-rows.ts +1 -1
  168. package/src/supplied-text.ts +3 -3
  169. package/src/thinking-drops.ts +2 -0
  170. package/src/tool-presentation.ts +34 -9
  171. package/src/tune-out/coordinator.ts +32 -15
  172. package/src/tune-out/tools.ts +2 -1
  173. package/src/types/agent.ts +36 -5
  174. package/src/types/framework.ts +44 -8
  175. package/src/types/inference-log.ts +1 -0
  176. package/src/types/trace.ts +66 -4
@@ -6,10 +6,14 @@
6
6
  * - **Legacy**: MCP 2024-11-05, with MCPL riding on it as an experimental
7
7
  * capability. `McplServerConnection` speaks it, over stdio or MCPL's
8
8
  * WebSocket binding.
9
- * - **Modern**: MCP 2026-07-28, which is stateless. It has no `initialize`;
10
- * every request carries its version and client capabilities in `_meta`, and
11
- * server-to-client requests are replaced by in-band continuation.
12
- * `ModernMcpConnection` speaks it, over stdio or Streamable HTTP.
9
+ * - **Modern**: the official SDK's client, which `ModernMcpConnection` drives.
10
+ * Over stdio it speaks MCP 2026-07-28, which is stateless. That revision
11
+ * has no `initialize`: every request carries its version and client
12
+ * capabilities in `_meta`, and server-to-client requests are replaced by
13
+ * in-band continuation. Over Streamable HTTP the revision is negotiated at
14
+ * each connect, from {@link MODERN_HTTP_PROTOCOL_VERSIONS}: 2026-07-28
15
+ * when the server offers it, and otherwise a 2025 revision, with its
16
+ * `initialize` handshake and its session.
13
17
  *
14
18
  * A URL's scheme decides a network server's family: `ws://`/`wss://` is
15
19
  * legacy, `http://`/`https://` is modern, because neither family has the
@@ -17,10 +21,10 @@
17
21
  * unless its config says `protocol: 'modern'`, so every existing stdio
18
22
  * configuration opens exactly as before.
19
23
  *
20
- * Why no auto-detection: the modern spec's discover-first probe makes some
21
- * real MCPL servers exit (heartbeat-mcpl closes on any first message that
22
- * isn't `initialize`), the SDK's stdio probe launches the server twice, and a
23
- * server's era is a stable fact its configuration can state.
24
+ * Why a stdio server's era isn't detected: the modern spec's discover-first
25
+ * probe makes some real MCPL servers exit (heartbeat-mcpl closes on any first
26
+ * message that isn't `initialize`), the SDK's stdio probe launches the server
27
+ * twice, and a server's era is a stable fact its configuration can state.
24
28
  */
25
29
  import { NOTICE_TAG } from '../notice-tags.js';
26
30
  import { DENY_BY_DEFAULT, capabilityPatternMatches } from './capability-grant.js';
@@ -31,8 +35,10 @@ import { kindOf } from '../config-checks.js';
31
35
 
32
36
  /** The one revision the legacy engine offers and accepts. */
33
37
  export const LEGACY_MCP_PROTOCOL_VERSION = '2024-11-05';
34
- /** The revision the modern engine pins. */
38
+ /** The revision the modern engine pins over stdio, and prefers over HTTP. */
35
39
  export const MODERN_MCP_PROTOCOL_VERSION = '2026-07-28';
40
+ /** 2024-11-05 isn't offered: its HTTP binding was HTTP+SSE, another transport. */
41
+ export const MODERN_HTTP_PROTOCOL_VERSIONS: readonly string[] = [MODERN_MCP_PROTOCOL_VERSION, '2025-11-25', '2025-06-18', '2025-03-26'];
36
42
 
37
43
  export type McpProtocolFamily = 'legacy' | 'modern';
38
44
  export type McpTransportKind = 'stdio' | 'websocket' | 'http';
@@ -157,20 +157,27 @@ const UNSUPPORTED_PROTOCOL_VERSION_CODE = -32022;
157
157
  const MAX_TOOL_LIST_PAGES = 100;
158
158
 
159
159
  /**
160
- * The server and this engine have no protocol revision in common, so the
161
- * connection was never established. This is a protocol-negotiation fact, not
162
- * a request outcome. Retrying cannot change it: each engine offers one
163
- * revision, and only configuration can fix the mismatch. A connection that
164
- * gets this error does not schedule reconnects. The legacy engine and the
165
- * modern one (ModernMcpConnection, whose pinned revision the server refused)
166
- * both throw it, so its readers treat them alike.
160
+ * This engine and the server could not negotiate a protocol revision, so the
161
+ * connect that throws it established no connection. Usually they share none. Over HTTP it
162
+ * can also be the SDK's own refusal: after a -32022 whose list names a
163
+ * revision from 2026-07-28 on that this client doesn't speak, the SDK
164
+ * negotiates no further, even when the list also names a 2025 revision both
165
+ * speak. This is a protocol-negotiation fact, not a request outcome.
166
+ * Retrying cannot change it: each engine offers the same revisions every
167
+ * time (`offeredRevisions`), and only configuration, or the server's
168
+ * operator, can fix the mismatch. A connection that gets this error does not
169
+ * schedule reconnects. The legacy engine and the modern one
170
+ * (ModernMcpConnection) both throw it, so its readers treat them alike.
167
171
  *
168
- * - `rejected`: the server refused the offered revision: the legacy engine's
169
- * `initialize` answered with error -32022, or a modern server's discovery
170
- * or error naming no common revision. `supported` is the server's own list,
171
- * when it gave one.
172
- * - `mismatch`: the server answered the legacy engine's `initialize` with a
173
- * revision other than 2024-11-05, or with none.
172
+ * - `rejected`: the server refused the revision offered. The legacy
173
+ * engine's `initialize` was answered with error -32022. A modern server's
174
+ * discovery or -32022 error named no revision in common (over HTTP, a
175
+ * discovery's list counts once the server has refused the `initialize`
176
+ * tried after it too), or, over HTTP, named one this client doesn't speak,
177
+ * as above. `supported` is the server's own list, when it gave one.
178
+ * - `mismatch`: the server answered `initialize` with a revision that
179
+ * handshake doesn't accept, or with none: the legacy engine's, or a modern
180
+ * connection's 2025 handshake over HTTP.
174
181
  */
175
182
  export class McplProtocolVersionError extends Error {
176
183
  constructor(
@@ -185,9 +192,15 @@ export class McplProtocolVersionError extends Error {
185
192
  * redacted (`rejected`). */
186
193
  readonly code?: number,
187
194
  readonly data?: unknown,
188
- /** The revision this client offered: the legacy engine's, unless a
189
- * modern connection's. */
195
+ /** The revision this client offered first, in its order of preference:
196
+ * the legacy engine's (2024-11-05), a modern connection's over stdio
197
+ * (2026-07-28), or over HTTP 2026-07-28, the first of
198
+ * `offeredRevisions`. */
190
199
  readonly offered: string = MCP_PROTOCOL_VERSION,
200
+ /** Every revision this client offered, in its order of preference: the
201
+ * legacy engine's one, a modern connection's one over stdio, or its
202
+ * list over HTTP. */
203
+ readonly offeredRevisions: readonly string[] = [offered],
191
204
  ) {
192
205
  super(message);
193
206
  this.name = 'McplProtocolVersionError';
@@ -27,6 +27,7 @@
27
27
  */
28
28
  import { objectSignature, renderToolLines, toolDigest, type HintedTool, type Shown } from './tool-signature.js';
29
29
  import { nameToTypeBack, quotedValue, suppliedValue } from '../supplied-text.js';
30
+ import { appendAll } from '../append-all.js';
30
31
 
31
32
  export interface ToolsetDecl {
32
33
  title?: string;
@@ -632,7 +633,7 @@ export class ToolPresence {
632
633
  continue;
633
634
  }
634
635
  const title = (c.opened || c.retitled) && !bare ? c.title : undefined;
635
- if (showsBlock(c)) lines.push(...this.toolBlock(c.key, c.now, { headed: !bare, title, seen, describeAll: opts.describeAll }));
636
+ if (showsBlock(c)) appendAll(lines, this.toolBlock(c.key, c.now, { headed: !bare, title, seen, describeAll: opts.describeAll }));
636
637
  else if (title) lines.push(`${this.label(c.key)} ${suppliedValue(title)}`);
637
638
  if (c.gone.length) lines.push(`now unavailable: ${c.gone.map(suppliedValue).join(', ')}`);
638
639
  if (c.opened && !bare) {
@@ -641,7 +642,8 @@ export class ToolPresence {
641
642
  }
642
643
  }
643
644
  for (const s of cs.suggest) lines.push(this.renderSuggestion(s));
644
- lines.push(...this.untoldNotes(cs), ...cs.notes);
645
+ appendAll(lines, this.untoldNotes(cs));
646
+ appendAll(lines, cs.notes);
645
647
  return lines.join('\n');
646
648
  }
647
649
 
@@ -663,7 +665,7 @@ export class ToolPresence {
663
665
  if (!v.size) return null;
664
666
  const lines: string[] = [];
665
667
  for (const [key, e] of v) {
666
- lines.push(...this.toolBlock(key, [...e.tools.values()], { describeAll: true, title: e.title }));
668
+ appendAll(lines, this.toolBlock(key, [...e.tools.values()], { describeAll: true, title: e.title }));
667
669
  const close = this.closeHint(key);
668
670
  if (close) lines.push(` ${close} closes it.`);
669
671
  }
@@ -679,7 +681,7 @@ export class ToolPresence {
679
681
  const v = this.view();
680
682
  if (v.size) {
681
683
  for (const [key, e] of v) {
682
- lines.push(...this.toolBlock(key, [...e.tools.values()], { describeAll: true, title: e.title, seen: opts.mark === false ? new Set<string>() : undefined }).map((l) => ` ${l}`));
684
+ appendAll(lines, this.toolBlock(key, [...e.tools.values()], { describeAll: true, title: e.title, seen: opts.mark === false ? new Set<string>() : undefined }).map((l) => ` ${l}`));
683
685
  const close = this.closeHint(key);
684
686
  if (close) lines.push(` ${close} closes it.`);
685
687
  }
@@ -700,7 +702,10 @@ export class ToolPresence {
700
702
  const names = this.allTools.filter((t) => this.toolsetOf(t.name) === g).map((t) => t.name);
701
703
  openable.push(` ${nameToTypeBack(g)}: ${names.map(nameToTypeBack).join(', ')}`);
702
704
  }
703
- if (openable.length) lines.push(`can open — ${this.names.open} {server:"${this.server}", toolset, instance?, options?}:`, ...openable);
705
+ if (openable.length) {
706
+ lines.push(`can open — ${this.names.open} {server:"${this.server}", toolset, instance?, options?}:`);
707
+ appendAll(lines, openable);
708
+ }
704
709
  return lines.join('\n');
705
710
  }
706
711
 
@@ -35,6 +35,7 @@ import {
35
35
  type ChangeSet, type MetaToolNames, type Presence,
36
36
  } from './tool-presence.js';
37
37
  import type { HintedTool } from './tool-signature.js';
38
+ import { appendAll } from '../append-all.js';
38
39
 
39
40
  /** The meta-tools' module, whose tools are `toolsets--open` and so on. */
40
41
  export const TOOLSETS_MODULE = 'toolsets';
@@ -246,7 +247,7 @@ export class ToolsetController {
246
247
  const toolset = typeof res.toolset === 'string' ? res.toolset : String(params.toolset ?? '');
247
248
  const instance = typeof res.instance === 'string' && res.instance ? res.instance : typeof params.instance === 'string' ? params.instance : undefined;
248
249
  this.presence.noteOpened(toolset, instance, typeof res.title === 'string' && res.title ? res.title : undefined);
249
- cs.changes.push(...this.presence.diff(before, this.presence.view()).changes);
250
+ appendAll(cs.changes, this.presence.diff(before, this.presence.view()).changes);
250
251
  break;
251
252
  }
252
253
  case 'toolsets/close': {
@@ -255,7 +256,7 @@ export class ToolsetController {
255
256
  cs = this.applyHints(metaHints(res), 'toolsets/close');
256
257
  const before = this.presence.view();
257
258
  this.presence.noteClosed(String(params.toolset ?? ''), typeof params.instance === 'string' ? params.instance : undefined);
258
- cs.changes.push(...this.presence.diff(before, this.presence.view()).changes);
259
+ appendAll(cs.changes, this.presence.diff(before, this.presence.view()).changes);
259
260
  break;
260
261
  }
261
262
  default:
@@ -298,7 +299,7 @@ export class ToolsetController {
298
299
  this.scheduleRecovery();
299
300
  }
300
301
  const cs = this.presence.diff(before, this.presence.view());
301
- cs.suggest.push(...r.suggest);
302
+ appendAll(cs.suggest, r.suggest);
302
303
  return cs;
303
304
  }
304
305
 
@@ -332,11 +333,11 @@ export class ToolsetController {
332
333
  this.scheduleRecovery();
333
334
  }
334
335
  const cs = this.presence.diff(before, this.presence.view());
335
- cs.suggest.push(...suggest);
336
+ appendAll(cs.suggest, suggest);
336
337
  // A lost toolset gets its note, not also a "closed" line.
337
338
  cs.changes = cs.changes.filter((c) => !lost.some((o) => keyOf(o.toolset, o.instance) === c.key));
338
339
  const told = this.presence.adoptionNotes(lost, ended);
339
- cs.notes.push(...told.notes);
340
+ appendAll(cs.notes, told.notes);
340
341
  if (told.ended.length) cs.ended = [...(cs.ended ?? []), ...told.ended];
341
342
  return cs;
342
343
  }
@@ -625,7 +626,7 @@ export class ToolsetController {
625
626
  const moved = this.presence.applyMoves(moves);
626
627
  if (!moved.length) return;
627
628
  const cs = emptyChange();
628
- cs.notes.push(...moved.map(({ name, to }) => to === 'dynamic'
629
+ appendAll(cs.notes, moved.map(({ name, to }) => to === 'dynamic'
629
630
  ? `${nameToTypeBack(name)} left your tool list: it's announced when available, and called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(name)}, args}.`
630
631
  : `${nameToTypeBack(name)} is in your tool list now, as ${nameToTypeBack(this.deps.listedName(this.serverId, name))}.`));
631
632
  this.deliver(cs);
@@ -32,6 +32,7 @@ import { keyOf, type ChangeSet, type ToolPresence } from './tool-presence.js';
32
32
  import { checkArgs, jsonText, renderToolDefinition, type HintedTool } from './tool-signature.js';
33
33
  import { McplRequestError } from './server-connection.js';
34
34
  import { BUILTIN_TOOLSET, type HostPresentation } from '../host-presentation.js';
35
+ import { appendAll } from '../append-all.js';
35
36
 
36
37
  /** The namespace a reminder's stored injection carries. */
37
38
  export const TOOLSETS_REMINDER_NAMESPACE = 'mcpl-toolsets';
@@ -516,7 +517,7 @@ export class ToolsetsModule implements Module {
516
517
  return said;
517
518
  })
518
519
  : [];
519
- lines.push(...unread);
520
+ appendAll(lines, unread);
520
521
  return lines.length ? textResult(`${result.data}\n${lines.join('\n')}`) : result;
521
522
  }
522
523
 
package/src/mcpl/types.ts CHANGED
@@ -267,8 +267,9 @@ export interface McplServerConfig {
267
267
  * - `ws://` or `wss://` is a legacy MCPL server over MCPL's WebSocket
268
268
  * binding. The host dials it and runs the same newline-delimited JSON-RPC
269
269
  * as over stdio.
270
- * - `http://` or `https://` is a modern MCP (2026-07-28) server over
271
- * Streamable HTTP.
270
+ * - `http://` or `https://` is a modern MCP server over Streamable HTTP.
271
+ * Its revision is negotiated at each connect: 2026-07-28 when the server
272
+ * offers it, and otherwise 2025-11-25, 2025-06-18 or 2025-03-26.
272
273
  */
273
274
  url?: string;
274
275
 
@@ -428,14 +429,34 @@ export interface McplServerConfig {
428
429
  * Only `true` turns it on, and null counts as not set. `create()`,
429
430
  * `connectMcplServer` and `restartMcplServer` refuse any other value, such
430
431
  * as a quoted "false".
432
+ *
433
+ * A modern server reached over HTTP at a 2025 revision can hold a session
434
+ * for this client. When the server ends it (HTTP 404), or stops serving its
435
+ * revision or its streams as a server upgraded to 2026-07-28 does, after it
436
+ * has answered a request, a new session starts at once whether or not this
437
+ * is set: the connection emits 'close' while its `willReconnect` reads
438
+ * true, then 'reconnect'. If that new session can't start, this setting
439
+ * decides, as for any lost connection, and a revision verdict stops it for
440
+ * good, as on any reconnect; without this setting, a second 'close', while
441
+ * `willReconnect` reads false, says the server is gone. An answered session
442
+ * that ends less than `reconnectIntervalMs` after it started gets a new
443
+ * one at once only if the last session restarted that way had lived that
444
+ * long, or there was none. A second such young end in a row, and a session
445
+ * that ends before it has answered, count as a failed connect, and follow
446
+ * this setting.
431
447
  */
432
448
  reconnect?: boolean;
433
449
 
434
450
  /**
435
451
  * Base interval between reconnection attempts in milliseconds.
436
452
  * The actual delay doubles with each consecutive failure (with ±25% jitter)
437
- * up to `reconnectMaxIntervalMs`, and resets after a successful handshake.
438
- * Default: 5000 (5 seconds).
453
+ * up to `reconnectMaxIntervalMs`, and resets after a successful handshake
454
+ * (for a modern server's 2025 session over HTTP, once that session's end
455
+ * starts a new one at once). Over HTTP the same backoff paces a modern
456
+ * server's streams: a dropped stream's first try waits the server's
457
+ * `retry` hint when it sent one above 0, and the backoff's first step
458
+ * otherwise, and a try after a failed one waits the longer of the hint
459
+ * and the doubling backoff. Default: 5000 (5 seconds).
439
460
  */
440
461
  reconnectIntervalMs?: number;
441
462
 
@@ -1172,7 +1172,7 @@ export class DiscordModule implements Module {
1172
1172
  // boundary whose tied millisecond must be preserved.
1173
1173
  const fetchedCompleteHistory = discordMessages.length < scrollback;
1174
1174
  const oldestFetchedTimestamp = discordMessages.length > 0
1175
- ? Math.min(...discordMessages.map((msg) => msg.timestamp.getTime()))
1175
+ ? discordMessages.reduce((min, msg) => Math.min(min, msg.timestamp.getTime()), Infinity)
1176
1176
  : undefined;
1177
1177
  for (const [externalId, rows] of storedMessageMap) {
1178
1178
  const stored = rows[0]!;
@@ -188,7 +188,7 @@ export class HealthModule implements Module {
188
188
  timestamp: safeZoned(s.timestamp),
189
189
  agentName: s.agentName,
190
190
  success: s.success,
191
- error: s.error,
191
+ error: s.errorWords,
192
192
  requestId: s.requestId,
193
193
  tokenUsage: s.tokenUsage,
194
194
  };
@@ -201,7 +201,7 @@ export class HealthModule implements Module {
201
201
  .map((s) => ({
202
202
  timestamp: s.timestamp,
203
203
  agentName: s.agentName,
204
- error: typeof s.error === 'string' ? safeSlice(s.error, 0, 400) : s.error,
204
+ error: typeof s.error === 'string' ? safeSlice(s.error, 0, 400) : null,
205
205
  requestId: s.requestId,
206
206
  }));
207
207
 
@@ -880,14 +880,14 @@ export class SemanticIndexer {
880
880
  // The current branch may then have items this pass didn't send, and
881
881
  // old items a failed delete left are still to go.
882
882
  if (left || oldItemsOwed) report.more = true;
883
- this.consecutiveFailures = 0; this.lastError = named(notStats?.message ?? null, ...phaseErrors); this.lastSyncAt = Date.now();
883
+ this.consecutiveFailures = 0; this.lastError = named([notStats?.message ?? null, ...phaseErrors]); this.lastSyncAt = Date.now();
884
884
  return report;
885
885
  } catch (e) {
886
886
  this.consecutiveFailures++;
887
887
  const delay = Math.min(15 * 60_000, 5_000 * 2 ** Math.min(8, this.consecutiveFailures - 1));
888
888
  this.backoffUntil = Date.now() + delay;
889
889
  const failure = e instanceof Error ? e.message : String(e);
890
- this.lastError = named(failure, notStats?.message ?? null, ...phaseErrors);
890
+ this.lastError = named([failure, notStats?.message ?? null, ...phaseErrors]);
891
891
  this.warn(`sync failed (${this.consecutiveFailures}×, retry in ${Math.round(delay / 1000)}s): ${failure}`);
892
892
  report.more = true;
893
893
  return report;
@@ -896,6 +896,6 @@ export class SemanticIndexer {
896
896
  }
897
897
 
898
898
  /** Each failure a sync pass met, once, in the order given, for its lastError: null if none. */
899
- function named(...failures: (string | null)[]): string | null {
899
+ function named(failures: (string | null)[]): string | null {
900
900
  return [...new Set(failures.filter((f): f is string => f !== null))].join('; ') || null;
901
901
  }
@@ -89,6 +89,7 @@ import {
89
89
  sameIdentity,
90
90
  stageContained,
91
91
  } from './direct-write.js';
92
+ import { appendAll } from '../../append-all.js';
92
93
 
93
94
  /** Default for how long the after-batch scan may hold the agent's next inference. */
94
95
  const AGENT_ACTION_SCAN_DEADLINE_MS = 20_000;
@@ -4549,7 +4550,7 @@ export class WorkspaceModule implements Module {
4549
4550
  });
4550
4551
  continue;
4551
4552
  }
4552
- notes.push(...lookedIn);
4553
+ appendAll(notes, lookedIn);
4553
4554
  const plan = planTargetEdits(tool, target, stored.bytes, hints, maxSize, counts, problems);
4554
4555
  if (plan === null) continue;
4555
4556
  plans.push({
@@ -5670,8 +5671,8 @@ export class WorkspaceModule implements Module {
5670
5671
  const found = mount.direct
5671
5672
  ? await this.textsFromDisk(mount, relativePath, root, start, kept, (text) => searched(text)())
5672
5673
  : await this.textsFromStore(mount, relativePath, start, kept, searched);
5673
- skipped.push(...found.skipped);
5674
- incomplete.push(...found.incomplete);
5674
+ appendAll(skipped, found.skipped);
5675
+ appendAll(incomplete, found.incomplete);
5675
5676
  }
5676
5677
 
5677
5678
  const answer: GrepAnswer = {
@@ -48,6 +48,7 @@ import type { FileHandle } from 'node:fs/promises';
48
48
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
49
49
  import { onlyDeviceDiffers, sameRootIdentity, type Fingerprint, type RootIdentity } from './disk-agreement.js';
50
50
  import { OUTSIDE_STEP, UNFOLLOWED_STEP, errnoClause, errnoCode, systemWords } from './answers.js';
51
+ import { appendAll } from '../../append-all.js';
51
52
 
52
53
  export interface MountView {
53
54
  /**
@@ -2391,7 +2392,7 @@ export async function walkScope(
2391
2392
  for (const d of subdirs) walk.dirs.add(d);
2392
2393
  for (const [p, why] of others) walk.others.set(p, why);
2393
2394
  for (const p of otherFiles) walk.otherFiles.add(p);
2394
- walk.incomplete.push(...regions);
2395
+ appendAll(walk.incomplete, regions);
2395
2396
  walk.complete.add(rel);
2396
2397
  const listing = { leaves, subdirs, ...dir.identity() };
2397
2398
  listings.set(rel, listing);
@@ -94,6 +94,7 @@ import {
94
94
  } from './observe.js';
95
95
  import { EffectFailed, type Expect, callFailed, chainOf, chmodContained, narrowedTo, syncDirectories, unlinkContained, writeContained } from './effects.js';
96
96
  import { OUTSIDE_STEP, RESOLVED_OUTSIDE_STEP, UNFOLLOWED_STEP, errnoClause, errnoWords } from './answers.js';
97
+ import { appendAll } from '../../append-all.js';
97
98
 
98
99
  export type EntryState =
99
100
  | 'synced'
@@ -1150,8 +1151,8 @@ export async function reconcilePass(
1150
1151
  return result;
1151
1152
  }
1152
1153
  const { walk, facts } = gathered;
1153
- result.dirs.push(...gathered.dirs);
1154
- result.incomplete.push(...gathered.incomplete);
1154
+ appendAll(result.dirs, gathered.dirs);
1155
+ appendAll(result.incomplete, gathered.incomplete);
1155
1156
 
1156
1157
  // Decide and apply — synchronously, so nothing interleaves.
1157
1158
  const branchId = store.currentBranch().id;
@@ -17,7 +17,7 @@ export const NOTICE_TAG = {
17
17
  turnInterrupted: 'turn-interrupted',
18
18
  closedChannel: 'closed channel',
19
19
  tunedOut: 'tuned out',
20
- refusalRewind: 'refusal-rewind',
20
+ refusalRewind: 'turns-set-aside',
21
21
  drafts: 'drafts',
22
22
  delivered: 'delivered',
23
23
  routing: 'routing',
@@ -75,20 +75,23 @@ export const NOTICE_TAG = {
75
75
  noText: 'no text',
76
76
  } as const;
77
77
 
78
+ export const FORMER_NOTICE_TAGS = ['refusal-rewind'] as const;
79
+
78
80
  /**
79
- * The bracket openings context-manager 0.23.0 and 0.23.1 write into an
80
- * agent's context outside a tool result or read back from it, marked as the
81
- * framework's own are. Check them against each context-manager release the
82
- * framework takes.
81
+ * The bracket openings context-manager 0.25.1 writes into an agent's context
82
+ * outside a tool result or read back from it, the same as 0.23.1 and 0.25.0
83
+ * write, marked as the framework's own are. Its `[truncated — original was`
84
+ * isn't here: NOTICE_TAG.truncated marks it. Check them against each
85
+ * context-manager release the framework takes.
83
86
  */
84
87
  export const CONTEXT_MANAGER_TAGS = [
85
88
  'content omitted', 'image not shown', 'Operator note', 'Recall', 'tool call omitted', 'tool result moved', 'Topic Transition',
86
89
  ] as const;
87
90
 
88
91
  /**
89
- * The bracket openings membrane 0.8.0 writes into an agent's request outside
90
- * a tool result, marked as the framework's own are. Check them against each
91
- * membrane release the framework takes.
92
+ * The bracket openings membrane 0.8.0 and 0.9.0 write into an agent's request
93
+ * outside a tool result, marked as the framework's own are. Check them against
94
+ * each membrane release the framework takes.
92
95
  */
93
96
  export const MEMBRANE_TAGS = [
94
97
  'Audio', 'continuing', 'Document', 'duplicate tool_result for', 'Image', 'orphan tool_result for', 'Start', 'Tool Result', 'Video',
@@ -63,6 +63,7 @@
63
63
  */
64
64
  import type { ContentBlock } from '@animalabs/membrane';
65
65
  import { continuesBody, MessageStore, type ContextManager, type MessageId, type MessageMetadata, type StoredMessage } from '@animalabs/context-manager';
66
+ import { isReasoningLoss } from './reasoning-loss.js';
66
67
  import { audienceOf, heldFromResidents, observedRowFor, suppressedRow } from './dendrite/audience.js';
67
68
  import { sameSlot, type AgentRegistry } from './dendrite/registry.js';
68
69
  import type { AgentName, AgentRecord, MessageSlotRef, ObserveEdge } from './dendrite/types.js';
@@ -228,7 +229,8 @@ export type RowSlot = 'own' | MessageSlotRef;
228
229
  * a ref sameSlot as its `ownSlot`), else a slot it may observe. undefined,
229
230
  * a slot its caller couldn't name, is elsewhere.
230
231
  *
231
- * A row a suppress rule hid (suppressedRow) is elsewhere to every reader,
232
+ * A legacy reasoning-loss notice or a row a suppress rule hid
233
+ * (suppressedRow) is elsewhere to every reader,
232
234
  * in any slot: no view the framework sets renders it.
233
235
  *
234
236
  * In its own slot, with `stamp = audienceOf(metadata)`:
@@ -263,7 +265,7 @@ export function bearingOf(
263
265
  ): Bearing {
264
266
  const metadata = row.metadata as Record<string, unknown> | undefined;
265
267
  if (slot === undefined) return 'elsewhere';
266
- if (suppressedRow(metadata)) return 'elsewhere';
268
+ if (isReasoningLoss(metadata) || suppressedRow(metadata)) return 'elsewhere';
267
269
  if (slot !== 'own' && !sameSlot(slot, reader.ownSlot)) {
268
270
  const shown = reader.observed.some((seen) => sameSlot(seen.slot, slot)
269
271
  && observedRowFor(seen.edge, row, { observer: reader.name, owners: seen.owners }));
@@ -0,0 +1,7 @@
1
+ /** #973: #938 stopped writing these notices, but old stores retain them.
2
+ * Exclude deterministically before selection, compression and thinking binding;
3
+ * never match text or change stored history. */
4
+ export function isReasoningLoss(metadata: unknown): boolean {
5
+ const mark = metadata as { kind?: unknown; system?: unknown } | null | undefined;
6
+ return mark?.kind === 'reasoning-loss' && mark.system === true;
7
+ }
@@ -16,6 +16,7 @@ import {
16
16
  type DiscordAwarenessRef,
17
17
  type DiscordMessageMetadataCarrier,
18
18
  } from './discord-awareness-outbox.js';
19
+ import { appendAll } from '../append-all.js';
19
20
 
20
21
  export interface OfflineRecoveryBranchOptions {
21
22
  storePath: string;
@@ -877,7 +878,7 @@ function buildSuppressionPlan(
877
878
  const intervals: SuppressionInterval[] = [...stamped];
878
879
  for (const id of messageIds) {
879
880
  const location = locations.get(id)!;
880
- intervals.push(...location.runs.map((run) => ({ ...run })));
881
+ appendAll(intervals, location.runs.map((run) => ({ ...run })));
881
882
  }
882
883
  for (const range of ranges) {
883
884
  const from = locations.get(range.fromMessageId)!;
@@ -896,7 +897,7 @@ function buildSuppressionPlan(
896
897
  fromId: earlier.firstId,
897
898
  toId: later.firstId,
898
899
  });
899
- for (const end of [from, to]) intervals.push(...end.runs.map((run) => ({ ...run })));
900
+ for (const end of [from, to]) appendAll(intervals, end.runs.map((run) => ({ ...run })));
900
901
  }
901
902
 
902
903
  intervals.sort((a, b) => a.start - b.start || a.end - b.end);
@@ -916,7 +917,7 @@ function buildSuppressionPlan(
916
917
  const carriers: DiscordMessageMetadataCarrier[] = [];
917
918
  const ids = new Set<string>();
918
919
  for (const interval of merged) {
919
- carriers.push(...collectDiscordCarriers(contextManager, interval.start, interval.end + 1, ids));
920
+ appendAll(carriers, collectDiscordCarriers(contextManager, interval.start, interval.end + 1, ids));
920
921
  }
921
922
  return {
922
923
  intervals: merged,
@@ -946,7 +947,7 @@ function collectDiscordCarriers(
946
947
  Math.min(RECOVERY_SCAN_WINDOW, end - offset),
947
948
  { resolveBlobs: false },
948
949
  ).messages;
949
- carriers.push(...messages.map(addressingOnly));
950
+ appendAll(carriers, messages.map(addressingOnly));
950
951
  if (ids) for (const message of messages) ids.add(String(message.id));
951
952
  }
952
953
  return carriers;
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { createOfflineRecoveryBranch } from './offline-branch.js';
4
4
  import type { DiscordAwarenessMarks } from './discord-awareness-outbox.js';
5
+ import { appendAll } from '../append-all.js';
5
6
 
6
7
  interface CliOptions {
7
8
  storePath?: string;
@@ -86,7 +87,7 @@ function parseArgs(args: string[]): CliOptions {
86
87
  case '--messages': options.messages = Number(value()); break;
87
88
  case '--suppress':
88
89
  case '--suppress-message':
89
- options.suppressMessageIds.push(...value().split(',').map((item) => item.trim()).filter(Boolean));
90
+ appendAll(options.suppressMessageIds, value().split(',').map((item) => item.trim()).filter(Boolean));
90
91
  break;
91
92
  case '--suppress-range': {
92
93
  const raw = value();
package/src/sent-rows.ts CHANGED
@@ -152,7 +152,7 @@ export function roundRows(
152
152
  * the two alike (read from the provider on 2026-10-23: Haiku 5.5 with the drop control, on an enforcing API-key
153
153
  * organization and on a subscription account, reported no drop for either, and dropped both replies' blocks when an
154
154
  * earlier user message changed). The store's own fields beside them, such as
155
- * `toolName`, don't reach the provider on membrane's native path, the only one noted (membrane 0.8.0's XML
155
+ * `toolName`, don't reach the provider on membrane's native path, the only one noted (membrane 0.8.0's and 0.9.0's XML
156
156
  * formatter reads `toolName`, and an XML stream is never noted).
157
157
  */
158
158
  function normalizedResult(block: ContentBlock): unknown {
@@ -5,7 +5,7 @@
5
5
  * a line that reads as the framework's own.
6
6
  */
7
7
 
8
- import { CONTEXT_MANAGER_TAGS, MEMBRANE_TAGS, NOTICE_TAG } from './notice-tags.js';
8
+ import { CONTEXT_MANAGER_TAGS, FORMER_NOTICE_TAGS, MEMBRANE_TAGS, NOTICE_TAG } from './notice-tags.js';
9
9
 
10
10
  /** Every form of line break a reader might take for one: CR LF, CR, LF,
11
11
  * vertical tab, form feed, NEL, and the Unicode line and paragraph separators. */
@@ -27,14 +27,14 @@ const SKIPPED = `[\\s\\p{Cc}${INVISIBLE}]`;
27
27
  const tagWord = (word: string): string =>
28
28
  [...word].map((c) => (/[\\^$.*+?()[\]{}|/]/.test(c) ? `\\${c}` : c)).join(`${SKIPPED}*`);
29
29
  const NOTICE_OPENING = new RegExp(
30
- `\\[(?=${SKIPPED}*(?:${[...Object.values(NOTICE_TAG), ...CONTEXT_MANAGER_TAGS, ...MEMBRANE_TAGS].map((tag) => tag.split(' ').map(tagWord).join(`${SKIPPED}+`)).join('|')})(?![\\p{L}\\p{N}]))`,
30
+ `\\[(?=${SKIPPED}*(?:${[...Object.values(NOTICE_TAG), ...FORMER_NOTICE_TAGS, ...CONTEXT_MANAGER_TAGS, ...MEMBRANE_TAGS].map((tag) => tag.split(' ').map(tagWord).join(`${SKIPPED}+`)).join('|')})(?![\\p{L}\\p{N}]))`,
31
31
  'giu',
32
32
  );
33
33
 
34
34
  /**
35
35
  * Texts read as one string, joined by line breaks, with a backslash put
36
36
  * before the bracket of each opening of a listed tag (NOTICE_TAG,
37
- * CONTEXT_MANAGER_TAGS, MEMBRANE_TAGS) that hasn't one before it in its own
37
+ * FORMER_NOTICE_TAGS, CONTEXT_MANAGER_TAGS, MEMBRANE_TAGS) that hasn't one before it in its own
38
38
  * text. A text `marks` says false of is read and never changed.
39
39
  *
40
40
  * Every opening, not only one that starts a line: membrane's XML formatter
@@ -34,6 +34,8 @@ export interface StreamThinkingDrops {
34
34
  place: (entries: readonly ThinkingBindingEntry[]) => PlacedDrop[];
35
35
  recordedAfter?: number;
36
36
  branch?: string;
37
+ /** `StartStreamResult`'s `noteThinkingDropped`. */
38
+ noteDropped?: (messageIds: readonly MessageId[]) => boolean;
37
39
  }
38
40
 
39
41
  /** A signed block's identity on the wire: a thinking block's signature, a redacted block's data. */