@animalabs/agent-framework 0.6.5 → 0.6.7

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 (155) hide show
  1. package/README.md +78 -0
  2. package/dist/src/agent.d.ts +56 -2
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +372 -57
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server.js +0 -0
  7. package/dist/src/api/server.d.ts.map +1 -1
  8. package/dist/src/api/server.js +7 -2
  9. package/dist/src/api/server.js.map +1 -1
  10. package/dist/src/canonical-json.d.ts +2 -0
  11. package/dist/src/canonical-json.d.ts.map +1 -0
  12. package/dist/src/canonical-json.js +23 -0
  13. package/dist/src/canonical-json.js.map +1 -0
  14. package/dist/src/framework.d.ts +160 -2
  15. package/dist/src/framework.d.ts.map +1 -1
  16. package/dist/src/framework.js +3098 -167
  17. package/dist/src/framework.js.map +1 -1
  18. package/dist/src/gate/event-gate.d.ts.map +1 -1
  19. package/dist/src/gate/event-gate.js +34 -4
  20. package/dist/src/gate/event-gate.js.map +1 -1
  21. package/dist/src/index.d.ts +4 -0
  22. package/dist/src/index.d.ts.map +1 -1
  23. package/dist/src/index.js +3 -0
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/mcpl/channel-registry.d.ts +36 -15
  26. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  27. package/dist/src/mcpl/channel-registry.js +322 -76
  28. package/dist/src/mcpl/channel-registry.js.map +1 -1
  29. package/dist/src/mcpl/push-handler.d.ts +1 -0
  30. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  31. package/dist/src/mcpl/push-handler.js +1 -0
  32. package/dist/src/mcpl/push-handler.js.map +1 -1
  33. package/dist/src/mcpl/server-connection.d.ts +39 -4
  34. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  35. package/dist/src/mcpl/server-connection.js +146 -28
  36. package/dist/src/mcpl/server-connection.js.map +1 -1
  37. package/dist/src/mcpl/types.d.ts +43 -12
  38. package/dist/src/mcpl/types.d.ts.map +1 -1
  39. package/dist/src/mcpl/types.js +1 -0
  40. package/dist/src/mcpl/types.js.map +1 -1
  41. package/dist/src/modules/workspace/index.d.ts +12 -1
  42. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  43. package/dist/src/modules/workspace/index.js +661 -2
  44. package/dist/src/modules/workspace/index.js.map +1 -1
  45. package/dist/src/modules/workspace/types.d.ts +4 -0
  46. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  47. package/dist/src/modules/workspace/types.js.map +1 -1
  48. package/dist/src/recovery/discord-awareness-outbox.d.ts +99 -0
  49. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -0
  50. package/dist/src/recovery/discord-awareness-outbox.js +334 -0
  51. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -0
  52. package/dist/src/recovery/offline-branch.d.ts +45 -0
  53. package/dist/src/recovery/offline-branch.d.ts.map +1 -0
  54. package/dist/src/recovery/offline-branch.js +362 -0
  55. package/dist/src/recovery/offline-branch.js.map +1 -0
  56. package/dist/src/recovery/recover-cli.d.ts +3 -0
  57. package/dist/src/recovery/recover-cli.d.ts.map +1 -0
  58. package/dist/src/recovery/recover-cli.js +129 -0
  59. package/dist/src/recovery/recover-cli.js.map +1 -0
  60. package/dist/src/types/agent.d.ts +37 -0
  61. package/dist/src/types/agent.d.ts.map +1 -1
  62. package/dist/src/types/events.d.ts +1 -0
  63. package/dist/src/types/events.d.ts.map +1 -1
  64. package/dist/src/types/framework.d.ts +14 -0
  65. package/dist/src/types/framework.d.ts.map +1 -1
  66. package/dist/src/types/module.d.ts +31 -0
  67. package/dist/src/types/module.d.ts.map +1 -1
  68. package/dist/src/types/trace.d.ts +4 -0
  69. package/dist/src/types/trace.d.ts.map +1 -1
  70. package/dist/test/agent-runtime-settings.test.d.ts +2 -0
  71. package/dist/test/agent-runtime-settings.test.d.ts.map +1 -0
  72. package/dist/test/agent-runtime-settings.test.js +227 -0
  73. package/dist/test/agent-runtime-settings.test.js.map +1 -0
  74. package/dist/test/busy-agent-scheduler.test.d.ts +2 -0
  75. package/dist/test/busy-agent-scheduler.test.d.ts.map +1 -0
  76. package/dist/test/busy-agent-scheduler.test.js +62 -0
  77. package/dist/test/busy-agent-scheduler.test.js.map +1 -0
  78. package/dist/test/channel-lifecycle-provenance.test.d.ts +2 -0
  79. package/dist/test/channel-lifecycle-provenance.test.d.ts.map +1 -0
  80. package/dist/test/channel-lifecycle-provenance.test.js +47 -0
  81. package/dist/test/channel-lifecycle-provenance.test.js.map +1 -0
  82. package/dist/test/channel-registry-routing.test.js +5 -4
  83. package/dist/test/channel-registry-routing.test.js.map +1 -1
  84. package/dist/test/conversation-fork-injection-scoping.test.js +12 -1
  85. package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
  86. package/dist/test/discord-awareness-outbox.test.d.ts +2 -0
  87. package/dist/test/discord-awareness-outbox.test.d.ts.map +1 -0
  88. package/dist/test/discord-awareness-outbox.test.js +248 -0
  89. package/dist/test/discord-awareness-outbox.test.js.map +1 -0
  90. package/dist/test/event-gate.test.js +21 -1
  91. package/dist/test/event-gate.test.js.map +1 -1
  92. package/dist/test/helpers/mock-membrane.d.ts +13 -1
  93. package/dist/test/helpers/mock-membrane.d.ts.map +1 -1
  94. package/dist/test/helpers/mock-membrane.js +9 -2
  95. package/dist/test/helpers/mock-membrane.js.map +1 -1
  96. package/dist/test/maintenance-timer.test.js +103 -1
  97. package/dist/test/maintenance-timer.test.js.map +1 -1
  98. package/dist/test/mcpl-awareness-barrier.test.d.ts +2 -0
  99. package/dist/test/mcpl-awareness-barrier.test.d.ts.map +1 -0
  100. package/dist/test/mcpl-awareness-barrier.test.js +845 -0
  101. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -0
  102. package/dist/test/mcpl-gate-roundtrip.test.js +138 -6
  103. package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
  104. package/dist/test/mcpl-reconnect-reregistration.test.js +22 -3
  105. package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
  106. package/dist/test/mcpl-request-timeout.test.js +15 -0
  107. package/dist/test/mcpl-request-timeout.test.js.map +1 -1
  108. package/dist/test/offline-recovery-branch.test.d.ts +2 -0
  109. package/dist/test/offline-recovery-branch.test.d.ts.map +1 -0
  110. package/dist/test/offline-recovery-branch.test.js +226 -0
  111. package/dist/test/offline-recovery-branch.test.js.map +1 -0
  112. package/dist/test/present-while-acting.test.d.ts +19 -0
  113. package/dist/test/present-while-acting.test.d.ts.map +1 -0
  114. package/dist/test/present-while-acting.test.js +295 -0
  115. package/dist/test/present-while-acting.test.js.map +1 -0
  116. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +2 -0
  117. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +1 -0
  118. package/dist/test/primary-summary-refusal-fallback-integration.test.js +1955 -0
  119. package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +1 -0
  120. package/dist/test/primary-summary-refusal-fallback.test.d.ts +2 -0
  121. package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +1 -0
  122. package/dist/test/primary-summary-refusal-fallback.test.js +753 -0
  123. package/dist/test/primary-summary-refusal-fallback.test.js.map +1 -0
  124. package/dist/test/same-round-think-policy.test.d.ts +2 -0
  125. package/dist/test/same-round-think-policy.test.d.ts.map +1 -0
  126. package/dist/test/same-round-think-policy.test.js +853 -0
  127. package/dist/test/same-round-think-policy.test.js.map +1 -0
  128. package/dist/test/trunk-channel-routing.test.js +38 -0
  129. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  130. package/dist/test/workspace-read-image.test.d.ts +2 -0
  131. package/dist/test/workspace-read-image.test.d.ts.map +1 -0
  132. package/dist/test/workspace-read-image.test.js +518 -0
  133. package/dist/test/workspace-read-image.test.js.map +1 -0
  134. package/dist/tsconfig.tsbuildinfo +1 -1
  135. package/package.json +6 -4
  136. package/src/agent.ts +485 -63
  137. package/src/api/server.ts +8 -2
  138. package/src/canonical-json.ts +22 -0
  139. package/src/framework.ts +4096 -304
  140. package/src/gate/event-gate.ts +50 -4
  141. package/src/index.ts +20 -0
  142. package/src/mcpl/channel-registry.ts +396 -85
  143. package/src/mcpl/push-handler.ts +2 -0
  144. package/src/mcpl/server-connection.ts +173 -28
  145. package/src/mcpl/types.ts +49 -12
  146. package/src/modules/workspace/index.ts +759 -2
  147. package/src/modules/workspace/types.ts +5 -0
  148. package/src/recovery/discord-awareness-outbox.ts +459 -0
  149. package/src/recovery/offline-branch.ts +510 -0
  150. package/src/recovery/recover-cli.ts +130 -0
  151. package/src/types/agent.ts +44 -0
  152. package/src/types/events.ts +1 -1
  153. package/src/types/framework.ts +17 -0
  154. package/src/types/module.ts +33 -0
  155. package/src/types/trace.ts +4 -0
@@ -35,6 +35,7 @@ export interface McplPushEvent {
35
35
  eventId: string;
36
36
  content: ContentBlock[];
37
37
  origin?: Record<string, unknown>;
38
+ tags?: string[];
38
39
  timestamp: string;
39
40
  inferenceId: string;
40
41
  triggerInference?: boolean;
@@ -217,6 +218,7 @@ export class PushHandler {
217
218
  eventId: params.eventId,
218
219
  content,
219
220
  origin: params.origin,
221
+ tags: params.tags,
220
222
  timestamp: params.timestamp,
221
223
  inferenceId,
222
224
  triggerInference,
@@ -31,6 +31,8 @@ import type {
31
31
  ChannelsOpenResult,
32
32
  ChannelsCloseParams,
33
33
  ChannelsCloseResult,
34
+ ChannelsAcknowledgeParams,
35
+ ChannelsAcknowledgeResult,
34
36
  ChannelsListResult,
35
37
  ChannelsPublishParams,
36
38
  ChannelsPublishResult,
@@ -67,6 +69,13 @@ interface PendingRequest {
67
69
  timer?: ReturnType<typeof setTimeout>;
68
70
  }
69
71
 
72
+ interface RequestOptions {
73
+ /** Override the connection-wide timeout for this request only. */
74
+ timeoutMs?: number;
75
+ /** Surface a late response even when it carries no checkpoint state. */
76
+ surfaceOrphanedResponse?: boolean;
77
+ }
78
+
70
79
  /**
71
80
  * McplServerConnection manages a single JSON-RPC 2.0 connection to an
72
81
  * MCPL server over stdio. Use the static `connect()` factory to create
@@ -105,13 +114,20 @@ export class McplServerConnection extends EventEmitter {
105
114
 
106
115
  private nextRequestId = 1;
107
116
  private pendingRequests = new Map<string | number, PendingRequest>();
117
+ /** Metadata retained after selected request deadlines so a late response is
118
+ * observable even though its original promise has already been rejected. */
119
+ private orphanedRequests = new Map<string | number, { method: string }>();
108
120
  private closed = false;
109
121
 
110
122
  /** Per-request timeout in ms (0 disables). See McplServerConfig.requestTimeoutMs. */
111
123
  private requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
112
124
 
113
- // Event buffering: events emitted before ready() are queued, not lost
114
- private readyFlag = false;
125
+ // Event buffering: control-plane traffic can be released independently from
126
+ // inference-bearing data-plane traffic. This lets a server finish its own
127
+ // registration (and answer host tools/call requests) while startup/reconnect
128
+ // reconciliation keeps agent wakes behind a durable barrier.
129
+ private controlPlaneReady = false;
130
+ private dataPlaneReady = false;
115
131
  private bufferedEvents: Array<{ event: string; args: unknown[] }> = [];
116
132
 
117
133
  // Reconnect state (adapted from Anarchid/agent-framework@mcpl-module-proto)
@@ -160,26 +176,87 @@ export class McplServerConnection extends EventEmitter {
160
176
  * the caller has attached listeners via wireMcplEvents).
161
177
  */
162
178
  ready(): void {
163
- this.readyFlag = true;
164
- for (const { event, args } of this.bufferedEvents) {
165
- super.emit(event, ...args);
179
+ this.controlPlaneReady = true;
180
+ this.dataPlaneReady = true;
181
+ const pending = this.bufferedEvents;
182
+ this.bufferedEvents = [];
183
+ for (const { event, args } of pending) {
184
+ // Re-enter the gate for every buffered item. A control handler (notably
185
+ // tools/list_changed) may install a newer data-plane barrier while this
186
+ // snapshot is being flushed; later data items must observe that pause.
187
+ this.emit(event, ...args);
166
188
  }
189
+ }
190
+
191
+ /**
192
+ * Release only server-establishment and lifecycle/control traffic. Responses
193
+ * to host requests are never event-buffered, so tools/list and tools/call
194
+ * responses remain live as well. Inference-bearing requests stay queued in
195
+ * their original relative order until {@link ready} is called.
196
+ */
197
+ readyControlPlane(): void {
198
+ this.controlPlaneReady = true;
199
+ const pending = this.bufferedEvents;
200
+ const deferred: Array<{ event: string; args: unknown[] }> = [];
167
201
  this.bufferedEvents = [];
202
+ for (const item of pending) {
203
+ if (McplServerConnection.isControlPlaneEvent(item.event)) {
204
+ super.emit(item.event, ...item.args);
205
+ } else {
206
+ deferred.push(item);
207
+ }
208
+ }
209
+ // Data events emitted by control handlers while the snapshot was flushing
210
+ // were appended to the new buffer. Preserve their order behind older data.
211
+ this.bufferedEvents = [...deferred, ...this.bufferedEvents];
168
212
  }
169
213
 
170
214
  /**
171
- * Override emit to buffer server→host events until ready() is called.
172
- * Lifecycle events ('close', 'error', 'reconnect') always pass through.
215
+ * Close the data plane synchronously while leaving an already-released
216
+ * control plane live. Used before reconnect adoption and list-change
217
+ * reconciliation so no new inference event can beat barrier installation.
218
+ */
219
+ pauseDataPlane(): void {
220
+ this.dataPlaneReady = false;
221
+ }
222
+
223
+ /**
224
+ * Override emit to buffer server→host events until the corresponding plane
225
+ * is ready. Lifecycle events always pass through so reconnect can install a
226
+ * new data-plane gate before the fresh transport is exposed.
173
227
  */
174
228
  override emit(event: string | symbol, ...args: unknown[]): boolean {
175
229
  const name = typeof event === 'string' ? event : '';
176
- if (this.readyFlag || name === 'close' || name === 'error' || name === 'reconnect') {
230
+ if (
231
+ McplServerConnection.isLifecycleEvent(name)
232
+ || (McplServerConnection.isControlPlaneEvent(name)
233
+ ? this.controlPlaneReady
234
+ : this.dataPlaneReady)
235
+ ) {
177
236
  return super.emit(event, ...args);
178
237
  }
179
238
  this.bufferedEvents.push({ event: name, args });
180
239
  return true;
181
240
  }
182
241
 
242
+ private static isControlPlaneEvent(event: string): boolean {
243
+ return event === 'stderr'
244
+ || event === 'scope-elevate'
245
+ || event === 'channels-register'
246
+ || event === 'channels-changed'
247
+ || event === 'feature-sets-changed'
248
+ || event === 'tools-list-changed'
249
+ || event === 'connect-failed'
250
+ || event === 'reconnect-failed'
251
+ || event === 'orphaned-response';
252
+ }
253
+
254
+ private static isLifecycleEvent(event: string): boolean {
255
+ return event === 'close'
256
+ || event === 'error'
257
+ || event === 'reconnect';
258
+ }
259
+
183
260
  // ==========================================================================
184
261
  // Static factory
185
262
  // ==========================================================================
@@ -397,6 +474,14 @@ export class McplServerConnection extends EventEmitter {
397
474
  return this.sendRequest(McplMethod.ChannelsClose, params as unknown as Record<string, unknown>) as Promise<ChannelsCloseResult>;
398
475
  }
399
476
 
477
+ /** Send `channels/acknowledge` request and await result. */
478
+ sendChannelsAcknowledge(params: ChannelsAcknowledgeParams): Promise<ChannelsAcknowledgeResult> {
479
+ return this.sendRequest(
480
+ McplMethod.ChannelsAcknowledge,
481
+ params as unknown as Record<string, unknown>,
482
+ ) as Promise<ChannelsAcknowledgeResult>;
483
+ }
484
+
400
485
  /** Send `channels/list` request and await result. */
401
486
  sendChannelsList(): Promise<ChannelsListResult> {
402
487
  return this.sendRequest(McplMethod.ChannelsList, {}) as Promise<ChannelsListResult>;
@@ -463,6 +548,26 @@ export class McplServerConnection extends EventEmitter {
463
548
  return this.sendRequest('tools/call', params) as Promise<McpToolCallResult>;
464
549
  }
465
550
 
551
+ /**
552
+ * Send a tools/call with a mandatory request-local deadline. This is used by
553
+ * durable external-side-effect accounting, whose safety bound must remain in
554
+ * force even when the server's general requestTimeoutMs is configured as 0.
555
+ */
556
+ sendToolsCallWithDeadline(
557
+ name: string,
558
+ args: Record<string, unknown>,
559
+ timeoutMs: number,
560
+ ): Promise<McpToolCallResult> {
561
+ const params: Record<string, unknown> = { name, arguments: args };
562
+ const mandatoryTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0
563
+ ? Math.max(1, Math.floor(timeoutMs))
564
+ : 1;
565
+ return this.sendRequest('tools/call', params, {
566
+ timeoutMs: mandatoryTimeoutMs,
567
+ surfaceOrphanedResponse: true,
568
+ }) as Promise<McpToolCallResult>;
569
+ }
570
+
466
571
  // ==========================================================================
467
572
  // Lifecycle
468
573
  // ==========================================================================
@@ -487,6 +592,7 @@ export class McplServerConnection extends EventEmitter {
487
592
  pending.reject(new Error(`Connection to MCPL server "${this.id}" closed while awaiting response for ${pending.method} (id=${id})`));
488
593
  }
489
594
  this.pendingRequests.clear();
595
+ this.orphanedRequests.clear();
490
596
 
491
597
  // Tear down the transport (kills the child / closes the socket). The
492
598
  // transport's own 'close' event is short-circuited by `this.closed` in
@@ -498,6 +604,27 @@ export class McplServerConnection extends EventEmitter {
498
604
  this.emit('close');
499
605
  }
500
606
 
607
+ /**
608
+ * Tear down a live transport after an awareness-accounting failure without
609
+ * disabling configured auto-reconnect. The normal transport close handler
610
+ * rejects requests, emits lifecycle state, and schedules the next bounded
611
+ * reconnect attempt. With reconnect disabled this leaves the server closed.
612
+ */
613
+ async reconnectAfterFailure(): Promise<void> {
614
+ this.pauseDataPlane();
615
+ if (this.closed) {
616
+ this.scheduleReconnect();
617
+ return;
618
+ }
619
+ if (this.transport) {
620
+ await this.transport.close();
621
+ return;
622
+ }
623
+ this.closed = true;
624
+ this.emit('close');
625
+ this.scheduleReconnect();
626
+ }
627
+
501
628
  /**
502
629
  * Schedule a background reconnection attempt.
503
630
  *
@@ -541,16 +668,17 @@ export class McplServerConnection extends EventEmitter {
541
668
  this.hostCapabilities,
542
669
  );
543
670
 
544
- // Adopt the new transport. Listeners were attached to external consumers
545
- // on the first wire, so mark ready immediately (re-wire re-binds the
546
- // transport-level handlers below).
671
+ // Close the data plane before the new transport can emit. The reconnect
672
+ // lifecycle event bypasses buffering; its framework listener installs the
673
+ // awareness barrier synchronously, then releases control traffic needed
674
+ // to establish the server while data remains held.
675
+ this.pauseDataPlane();
547
676
  this.transport = transport;
548
677
  this.capabilities = capabilities;
549
678
  this.closed = false;
550
679
  this.nextRequestId = 1;
551
680
  this.pendingRequests.clear();
552
681
  this.wireTransport(transport);
553
- this.readyFlag = true;
554
682
 
555
683
  console.error(`MCPL server "${this.id}" reconnected successfully`);
556
684
  this.reconnectAttempts = 0;
@@ -578,7 +706,11 @@ export class McplServerConnection extends EventEmitter {
578
706
  * the framework maps the rejection to an isError tool_result, so a hung tool
579
707
  * surfaces as a normal tool error and the turn completes.
580
708
  */
581
- private sendRequest(method: string, params: Record<string, unknown>): Promise<unknown> {
709
+ private sendRequest(
710
+ method: string,
711
+ params: Record<string, unknown>,
712
+ options: RequestOptions = {},
713
+ ): Promise<unknown> {
582
714
  if (this.closed) {
583
715
  return Promise.reject(new Error(`Cannot send request: connection to "${this.id}" is closed`));
584
716
  }
@@ -593,18 +725,22 @@ export class McplServerConnection extends EventEmitter {
593
725
 
594
726
  return new Promise((resolve, reject) => {
595
727
  const pending: PendingRequest = { resolve, reject, method };
596
- if (this.requestTimeoutMs > 0) {
728
+ const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
729
+ if (timeoutMs > 0) {
597
730
  pending.timer = setTimeout(() => {
598
731
  this.pendingRequests.delete(id);
732
+ if (options.surfaceOrphanedResponse) {
733
+ this.orphanedRequests.set(id, { method });
734
+ }
599
735
  reject(new Error(
600
736
  `MCPL server "${this.id}" did not respond to ${method} (id=${id}) ` +
601
- `within ${this.requestTimeoutMs}ms — the server may be hung. ` +
602
- `The request was abandoned; the connection remains open. Note the ` +
603
- `tool may still have completed server-side (this is only a response ` +
604
- `timeout, not a cancellation) — verify state before retrying, as a ` +
605
- `blind retry of a stateful/side-effecting tool may duplicate it.`,
737
+ `within ${timeoutMs}ms — the server may be hung. The response ` +
738
+ `outcome is unknown: the request was abandoned locally but was not ` +
739
+ `cancelled, so the tool may still have completed server-side; verify ` +
740
+ `state before retrying; a blind retry of a stateful/side-effecting ` +
741
+ `tool may duplicate it.`,
606
742
  ));
607
- }, this.requestTimeoutMs);
743
+ }, timeoutMs);
608
744
  // Don't hold the event loop open for the watchdog alone.
609
745
  pending.timer.unref?.();
610
746
  }
@@ -712,23 +848,31 @@ export class McplServerConnection extends EventEmitter {
712
848
  const pending = this.pendingRequests.get(response.id);
713
849
  if (!pending) {
714
850
  // Orphaned response — the request already timed out (or was settled) and
715
- // was removed from pendingRequests. Stateless late results are harmless to
716
- // drop, but a late STATEFUL result carries a `state`/`checkpoint` the
717
- // server has already advanced to: dropping it silently diverges the host's
718
- // checkpoint tree from the server's, and every subsequent call then sends
719
- // stale state. We can't safely re-inject it here (the dispatch context is
720
- // gone), but we surface it so the divergence is greppable instead of
721
- // invisible.
851
+ // was removed from pendingRequests. A late STATEFUL result must be
852
+ // surfaced because dropping it silently diverges the checkpoint tree.
853
+ // Selected side-effecting calls are also surfaced even without state so
854
+ // their outcome-unknown receipt remains visible. We cannot safely
855
+ // re-inject either result because the original dispatch context is gone.
856
+ const orphaned = this.orphanedRequests.get(response.id);
857
+ this.orphanedRequests.delete(response.id);
722
858
  const result = response.result;
723
859
  if (result && typeof result === 'object') {
724
860
  const r = result as { state?: unknown; checkpoint?: unknown };
725
- if (r.state !== undefined || r.checkpoint !== undefined) {
861
+ if (orphaned || r.state !== undefined || r.checkpoint !== undefined) {
726
862
  this.emit('orphaned-response', {
727
863
  id: response.id,
864
+ method: orphaned?.method,
728
865
  hadState: r.state !== undefined,
729
866
  hadCheckpoint: r.checkpoint !== undefined,
730
867
  });
731
868
  }
869
+ } else if (orphaned) {
870
+ this.emit('orphaned-response', {
871
+ id: response.id,
872
+ method: orphaned.method,
873
+ hadState: false,
874
+ hadCheckpoint: false,
875
+ });
732
876
  }
733
877
  return; // Orphaned response — ignore
734
878
  }
@@ -795,6 +939,7 @@ export class McplServerConnection extends EventEmitter {
795
939
  );
796
940
  }
797
941
  this.pendingRequests.clear();
942
+ this.orphanedRequests.clear();
798
943
 
799
944
  this.emit('close', info.code ?? null, info.signal ?? null);
800
945
 
package/src/mcpl/types.ts CHANGED
@@ -284,18 +284,9 @@ export interface McplServerConfig {
284
284
  toolPrefix?: string;
285
285
 
286
286
  /**
287
- * How channels registered by this server are opened.
288
- * - `'auto'` (default) — every registered channel is auto-opened; the host
289
- * receives all traffic. Matches prior behavior. Right for agents that
290
- * should listen passively (e.g. a frontdesk clerk).
291
- * - `'manual'` — registered channels are listed but not opened. The agent
292
- * must call the `channel_open` tool to start receiving messages. Right
293
- * for focused agents that should not be flooded with chatter from
294
- * servers that advertise many public channels (e.g. Zulip's full stream
295
- * list).
296
- * - `string[]` — allow-list of channel ids; only matching channels are
297
- * auto-opened. Ids are server-scoped (the raw id the server registers,
298
- * e.g. `"zulip:tracker-miner-f"`).
287
+ * @deprecated One-time migration input for an installation's old auto,
288
+ * manual, or allow-list policy. Runtime lifecycle state is persisted in
289
+ * Chronicle and changed with channel_open/channel_close.
299
290
  */
300
291
  channelSubscription?: 'auto' | 'manual' | string[];
301
292
  }
@@ -797,6 +788,33 @@ export interface ChannelDescriptor {
797
788
 
798
789
  /** Arbitrary metadata */
799
790
  metadata?: Record<string, unknown>;
791
+
792
+ /**
793
+ * Server bootstrap preference, consulted only when the host has no durable
794
+ * desired state for this channel.
795
+ */
796
+ initiallyOpen?: boolean;
797
+
798
+ /** Optional generic capabilities exposed by this channel. */
799
+ capabilities?: ChannelCapabilities;
800
+ }
801
+
802
+ export interface ChannelCapabilities {
803
+ history?: {
804
+ maxMessages?: number;
805
+ supportsBeforeMessage?: boolean;
806
+ supportsSinceLastSeen?: boolean;
807
+ };
808
+ acknowledgment?: {
809
+ kind?: string;
810
+ supportsValue?: boolean;
811
+ };
812
+ }
813
+
814
+ export interface ChannelHistoryRequest {
815
+ limit: number;
816
+ beforeMessageId?: string;
817
+ sinceLastSeen?: boolean;
800
818
  }
801
819
 
802
820
  /**
@@ -858,9 +876,12 @@ export interface ChannelsListResult {
858
876
  * Spec Section 14.3.
859
877
  */
860
878
  export interface ChannelsOpenParams {
879
+ /** Exact registered id. Preferred over type/address matching. */
880
+ channelId?: string;
861
881
  type: string;
862
882
  address?: Record<string, unknown>;
863
883
  metadata?: Record<string, unknown>;
884
+ history?: ChannelHistoryRequest;
864
885
  }
865
886
 
866
887
  /**
@@ -868,6 +889,8 @@ export interface ChannelsOpenParams {
868
889
  */
869
890
  export interface ChannelsOpenResult {
870
891
  channel: ChannelDescriptor;
892
+ history?: ChannelIncomingMessage[];
893
+ historyTruncated?: boolean;
871
894
  }
872
895
 
873
896
  /**
@@ -885,6 +908,19 @@ export interface ChannelsCloseResult {
885
908
  closed: boolean;
886
909
  }
887
910
 
911
+ export interface ChannelsAcknowledgeParams {
912
+ channelId: string;
913
+ messageId: string;
914
+ intent: string;
915
+ value?: string;
916
+ }
917
+
918
+ export interface ChannelsAcknowledgeResult {
919
+ acknowledged: boolean;
920
+ representation?: string;
921
+ reason?: string;
922
+ }
923
+
888
924
  /**
889
925
  * channels/outgoing/chunk params (Host → Server, Notification).
890
926
  * For observers receiving moderated deltas.
@@ -1046,6 +1082,7 @@ export const McplMethod = {
1046
1082
  ChannelsList: 'channels/list',
1047
1083
  ChannelsOpen: 'channels/open',
1048
1084
  ChannelsClose: 'channels/close',
1085
+ ChannelsAcknowledge: 'channels/acknowledge',
1049
1086
  ChannelsOutgoingChunk: 'channels/outgoing/chunk',
1050
1087
  ChannelsOutgoingComplete: 'channels/outgoing/complete',
1051
1088
  ChannelsPublish: 'channels/publish',