@animalabs/agent-framework 0.5.7 → 0.6.2

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 (179) hide show
  1. package/dist/src/agent.d.ts +4 -0
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +15 -0
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/framework.d.ts +243 -0
  6. package/dist/src/framework.d.ts.map +1 -1
  7. package/dist/src/framework.js +1378 -114
  8. package/dist/src/framework.js.map +1 -1
  9. package/dist/src/gate/event-gate.d.ts +64 -2
  10. package/dist/src/gate/event-gate.d.ts.map +1 -1
  11. package/dist/src/gate/event-gate.js +212 -12
  12. package/dist/src/gate/event-gate.js.map +1 -1
  13. package/dist/src/gate/gate-script-worker.d.ts +2 -0
  14. package/dist/src/gate/gate-script-worker.d.ts.map +1 -0
  15. package/dist/src/gate/gate-script-worker.js +69 -0
  16. package/dist/src/gate/gate-script-worker.js.map +1 -0
  17. package/dist/src/gate/gate-script.d.ts +44 -0
  18. package/dist/src/gate/gate-script.d.ts.map +1 -0
  19. package/dist/src/gate/gate-script.js +164 -0
  20. package/dist/src/gate/gate-script.js.map +1 -0
  21. package/dist/src/gate/types.d.ts +18 -3
  22. package/dist/src/gate/types.d.ts.map +1 -1
  23. package/dist/src/index.d.ts +3 -0
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/index.js +4 -0
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/mcpl/channel-registry.d.ts +60 -2
  28. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  29. package/dist/src/mcpl/channel-registry.js +114 -9
  30. package/dist/src/mcpl/channel-registry.js.map +1 -1
  31. package/dist/src/mcpl/checkpoint-manager.d.ts +7 -0
  32. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  33. package/dist/src/mcpl/checkpoint-manager.js +15 -0
  34. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  35. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  36. package/dist/src/mcpl/push-handler.js +5 -0
  37. package/dist/src/mcpl/push-handler.js.map +1 -1
  38. package/dist/src/mcpl/server-connection.d.ts +64 -30
  39. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  40. package/dist/src/mcpl/server-connection.js +247 -210
  41. package/dist/src/mcpl/server-connection.js.map +1 -1
  42. package/dist/src/mcpl/transport.d.ts +98 -0
  43. package/dist/src/mcpl/transport.d.ts.map +1 -0
  44. package/dist/src/mcpl/transport.js +281 -0
  45. package/dist/src/mcpl/transport.js.map +1 -0
  46. package/dist/src/mcpl/types.d.ts +59 -3
  47. package/dist/src/mcpl/types.d.ts.map +1 -1
  48. package/dist/src/mcpl/types.js.map +1 -1
  49. package/dist/src/module-registry.d.ts +17 -1
  50. package/dist/src/module-registry.d.ts.map +1 -1
  51. package/dist/src/module-registry.js +101 -6
  52. package/dist/src/module-registry.js.map +1 -1
  53. package/dist/src/prose-segments.d.ts +18 -0
  54. package/dist/src/prose-segments.d.ts.map +1 -0
  55. package/dist/src/prose-segments.js +39 -0
  56. package/dist/src/prose-segments.js.map +1 -0
  57. package/dist/src/runtime/liveness-watchdog-worker.d.ts +2 -0
  58. package/dist/src/runtime/liveness-watchdog-worker.d.ts.map +1 -0
  59. package/dist/src/runtime/liveness-watchdog-worker.js +57 -0
  60. package/dist/src/runtime/liveness-watchdog-worker.js.map +1 -0
  61. package/dist/src/runtime/liveness-watchdog.d.ts +36 -0
  62. package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -0
  63. package/dist/src/runtime/liveness-watchdog.js +106 -0
  64. package/dist/src/runtime/liveness-watchdog.js.map +1 -0
  65. package/dist/src/types/agent.d.ts +47 -0
  66. package/dist/src/types/agent.d.ts.map +1 -1
  67. package/dist/src/types/framework.d.ts +14 -0
  68. package/dist/src/types/framework.d.ts.map +1 -1
  69. package/dist/src/types/module.d.ts +58 -0
  70. package/dist/src/types/module.d.ts.map +1 -1
  71. package/dist/src/types/trace.d.ts +42 -0
  72. package/dist/src/types/trace.d.ts.map +1 -1
  73. package/dist/test/agent-cache-ttl.test.d.ts +2 -0
  74. package/dist/test/agent-cache-ttl.test.d.ts.map +1 -0
  75. package/dist/test/agent-cache-ttl.test.js +25 -0
  76. package/dist/test/agent-cache-ttl.test.js.map +1 -0
  77. package/dist/test/channel-registry-routing.test.js +108 -1
  78. package/dist/test/channel-registry-routing.test.js.map +1 -1
  79. package/dist/test/checkpoint-attribution.test.d.ts +2 -0
  80. package/dist/test/checkpoint-attribution.test.d.ts.map +1 -0
  81. package/dist/test/checkpoint-attribution.test.js +40 -0
  82. package/dist/test/checkpoint-attribution.test.js.map +1 -0
  83. package/dist/test/conversation-fork-injection-scoping.test.d.ts +2 -0
  84. package/dist/test/conversation-fork-injection-scoping.test.d.ts.map +1 -0
  85. package/dist/test/conversation-fork-injection-scoping.test.js +184 -0
  86. package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -0
  87. package/dist/test/event-gate-policy-mutation.test.d.ts +2 -0
  88. package/dist/test/event-gate-policy-mutation.test.d.ts.map +1 -0
  89. package/dist/test/event-gate-policy-mutation.test.js +137 -0
  90. package/dist/test/event-gate-policy-mutation.test.js.map +1 -0
  91. package/dist/test/event-gate-script.test.d.ts +2 -0
  92. package/dist/test/event-gate-script.test.d.ts.map +1 -0
  93. package/dist/test/event-gate-script.test.js +95 -0
  94. package/dist/test/event-gate-script.test.js.map +1 -0
  95. package/dist/test/event-gate-tags.test.d.ts +2 -0
  96. package/dist/test/event-gate-tags.test.d.ts.map +1 -0
  97. package/dist/test/event-gate-tags.test.js +76 -0
  98. package/dist/test/event-gate-tags.test.js.map +1 -0
  99. package/dist/test/gather-context-timeout.test.d.ts +2 -0
  100. package/dist/test/gather-context-timeout.test.d.ts.map +1 -0
  101. package/dist/test/gather-context-timeout.test.js +120 -0
  102. package/dist/test/gather-context-timeout.test.js.map +1 -0
  103. package/dist/test/inference-exhaustion-rewind.test.d.ts +2 -0
  104. package/dist/test/inference-exhaustion-rewind.test.d.ts.map +1 -0
  105. package/dist/test/inference-exhaustion-rewind.test.js +181 -0
  106. package/dist/test/inference-exhaustion-rewind.test.js.map +1 -0
  107. package/dist/test/inference-failure-observability.test.js +4 -0
  108. package/dist/test/inference-failure-observability.test.js.map +1 -1
  109. package/dist/test/liveness-watchdog.test.d.ts +2 -0
  110. package/dist/test/liveness-watchdog.test.d.ts.map +1 -0
  111. package/dist/test/liveness-watchdog.test.js +78 -0
  112. package/dist/test/liveness-watchdog.test.js.map +1 -0
  113. package/dist/test/mcpl-reconnect-events.test.d.ts +2 -0
  114. package/dist/test/mcpl-reconnect-events.test.d.ts.map +1 -0
  115. package/dist/test/mcpl-reconnect-events.test.js +119 -0
  116. package/dist/test/mcpl-reconnect-events.test.js.map +1 -0
  117. package/dist/test/mcpl-reconnect-reregistration.test.d.ts +2 -0
  118. package/dist/test/mcpl-reconnect-reregistration.test.d.ts.map +1 -0
  119. package/dist/test/mcpl-reconnect-reregistration.test.js +181 -0
  120. package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -0
  121. package/dist/test/mcpl-request-timeout.test.d.ts +2 -0
  122. package/dist/test/mcpl-request-timeout.test.d.ts.map +1 -0
  123. package/dist/test/mcpl-request-timeout.test.js +123 -0
  124. package/dist/test/mcpl-request-timeout.test.js.map +1 -0
  125. package/dist/test/mcpl-runtime-lifecycle.test.d.ts +7 -0
  126. package/dist/test/mcpl-runtime-lifecycle.test.d.ts.map +1 -0
  127. package/dist/test/mcpl-runtime-lifecycle.test.js +142 -0
  128. package/dist/test/mcpl-runtime-lifecycle.test.js.map +1 -0
  129. package/dist/test/mcpl-ws-transport.test.d.ts +2 -0
  130. package/dist/test/mcpl-ws-transport.test.d.ts.map +1 -0
  131. package/dist/test/mcpl-ws-transport.test.js +155 -0
  132. package/dist/test/mcpl-ws-transport.test.js.map +1 -0
  133. package/dist/test/prose-segments.test.d.ts +2 -0
  134. package/dist/test/prose-segments.test.d.ts.map +1 -0
  135. package/dist/test/prose-segments.test.js +52 -0
  136. package/dist/test/prose-segments.test.js.map +1 -0
  137. package/dist/test/refusal-rewind.test.d.ts +2 -0
  138. package/dist/test/refusal-rewind.test.d.ts.map +1 -0
  139. package/dist/test/refusal-rewind.test.js +108 -0
  140. package/dist/test/refusal-rewind.test.js.map +1 -0
  141. package/dist/test/state-scaling-e2e.test.d.ts +22 -0
  142. package/dist/test/state-scaling-e2e.test.d.ts.map +1 -0
  143. package/dist/test/state-scaling-e2e.test.js +200 -0
  144. package/dist/test/state-scaling-e2e.test.js.map +1 -0
  145. package/dist/test/state-scaling.test.d.ts +12 -0
  146. package/dist/test/state-scaling.test.d.ts.map +1 -0
  147. package/dist/test/state-scaling.test.js +243 -0
  148. package/dist/test/state-scaling.test.js.map +1 -0
  149. package/dist/test/tool-result-content.test.d.ts +2 -0
  150. package/dist/test/tool-result-content.test.d.ts.map +1 -0
  151. package/dist/test/tool-result-content.test.js +37 -0
  152. package/dist/test/tool-result-content.test.js.map +1 -0
  153. package/dist/test/trunk-channel-routing.test.d.ts +19 -0
  154. package/dist/test/trunk-channel-routing.test.d.ts.map +1 -0
  155. package/dist/test/trunk-channel-routing.test.js +175 -0
  156. package/dist/test/trunk-channel-routing.test.js.map +1 -0
  157. package/dist/tsconfig.tsbuildinfo +1 -1
  158. package/package.json +4 -3
  159. package/src/agent.ts +16 -0
  160. package/src/framework.ts +1576 -121
  161. package/src/gate/event-gate.ts +209 -12
  162. package/src/gate/gate-script-worker.ts +73 -0
  163. package/src/gate/gate-script.ts +180 -0
  164. package/src/gate/types.ts +19 -3
  165. package/src/index.ts +10 -0
  166. package/src/mcpl/channel-registry.ts +143 -8
  167. package/src/mcpl/checkpoint-manager.ts +16 -0
  168. package/src/mcpl/push-handler.ts +5 -0
  169. package/src/mcpl/server-connection.ts +262 -227
  170. package/src/mcpl/transport.ts +307 -0
  171. package/src/mcpl/types.ts +68 -3
  172. package/src/module-registry.ts +126 -6
  173. package/src/prose-segments.ts +42 -0
  174. package/src/runtime/liveness-watchdog-worker.ts +66 -0
  175. package/src/runtime/liveness-watchdog.ts +122 -0
  176. package/src/types/agent.ts +48 -0
  177. package/src/types/framework.ts +15 -0
  178. package/src/types/module.ts +65 -0
  179. package/src/types/trace.ts +51 -0
package/src/index.ts CHANGED
@@ -63,6 +63,16 @@ export type { GateConfig, GateOptions, GatePolicy, GatePolicyMatch, GateBehavior
63
63
  // MCPL channel registry (exposed for modules that need channel-level operations)
64
64
  export { ChannelRegistry } from './mcpl/index.js';
65
65
 
66
+ // MCPL server config (exposed for hosts that manage servers at runtime via
67
+ // connectMcplServer / disconnectMcplServer / restartMcplServer)
68
+ export type { McplServerConfig } from './mcpl/index.js';
69
+
70
+ // MCPL client connection (exposed so external tooling — server playtests,
71
+ // health probes, protocol harnesses — can dial an MCPL server exactly the
72
+ // way the framework host does, over stdio or WebSocket)
73
+ export { McplServerConnection } from './mcpl/index.js';
74
+ export type { McplHostCapabilities } from './mcpl/index.js';
75
+
66
76
  // Per-channel conversation routing
67
77
  export { ConversationRouter, DEFAULT_CLOSURE_PROMPT } from './mcpl/index.js';
68
78
  export type {
@@ -234,6 +234,31 @@ interface ChannelRegistryOptions {
234
234
  reason: string;
235
235
  textLen: number;
236
236
  }) => void;
237
+ /**
238
+ * Resolve a conversation fork's HOME channel from its agent name. Conversation
239
+ * forks are spawned bound to a single channel (the framework tracks this in
240
+ * `conversationAgentHomes` / `ConversationRouter.channelForAgent`); their
241
+ * plain-text speech must route THERE. The process-global `defaultPublishChannel`
242
+ * tracks only the most-recent inbound across ALL channels, so with one fork per
243
+ * channel running concurrently it misroutes a fork's reply to whichever channel
244
+ * last spoke (item 3). Returns undefined for the trunk/primary agent, which has
245
+ * no home and correctly falls back to the global locus (heartbeats, etc.).
246
+ */
247
+ homeChannelResolver?: (agentName: string) => string | undefined;
248
+ /**
249
+ * Resolve the channel that triggered an agent's CURRENT inference turn, by
250
+ * agent name. This is the fix for single-TRUNK agents (the only mode
251
+ * connectome-host runs — it never exposes conversation forks). A trunk agent
252
+ * has no fork home, so without this its plain-text speech falls back to the
253
+ * process-global `defaultPublishChannel`, which tracks the most-recent inbound
254
+ * across ALL channels and misroutes a reply to whichever channel last spoke
255
+ * under concurrency (item-3 redux). The framework tracks the triggering
256
+ * channel of the live turn per-agent and exposes it here; resolves to
257
+ * undefined for a heartbeat / no-trigger turn, which correctly keeps the
258
+ * global fallback. Consulted AFTER `homeChannelResolver` (a fork's home always
259
+ * wins), BEFORE `defaultPublishChannel`.
260
+ */
261
+ activeChannelResolver?: (agentName: string) => string | undefined;
237
262
  }
238
263
 
239
264
  // ============================================================================
@@ -258,6 +283,8 @@ export class ChannelRegistry {
258
283
  reason: string;
259
284
  textLen: number;
260
285
  }) => void;
286
+ private homeChannelResolver?: (agentName: string) => string | undefined;
287
+ private activeChannelResolver?: (agentName: string) => string | undefined;
261
288
 
262
289
  /** Registered channels, keyed by `{serverId}:{channelId}`. */
263
290
  private channels = new Map<string, ChannelEntry>();
@@ -304,6 +331,8 @@ export class ChannelRegistry {
304
331
  this.sendTypingFn = options?.sendTypingFn;
305
332
  this.shouldTriggerInference = options?.shouldTriggerInference;
306
333
  this.onRouteFailure = options?.onRouteFailure;
334
+ this.homeChannelResolver = options?.homeChannelResolver;
335
+ this.activeChannelResolver = options?.activeChannelResolver;
307
336
  }
308
337
 
309
338
  /**
@@ -484,6 +513,7 @@ export class ChannelRegistry {
484
513
  messageId: message.messageId,
485
514
  threadId: message.threadId,
486
515
  author: message.author,
516
+ ...(message.tags ? { tags: message.tags } : {}),
487
517
  },
488
518
  );
489
519
  }
@@ -525,6 +555,50 @@ export class ChannelRegistry {
525
555
  });
526
556
  }
527
557
 
558
+ /**
559
+ * Ensure a channel is registered AND open so it can serve as an outbound
560
+ * routing locus, lazy-registering it if unseen and opening it if closed.
561
+ *
562
+ * Mirrors the lazy-registration inside handleIncoming(), but for channels
563
+ * that only ever arrive as push/events rather than channels/incoming — the
564
+ * motivating case is Discord DMs, which discord-mcpl forwards via push/event
565
+ * with the channel closed (`channelIsOpen:false`). Such a channel is never
566
+ * registered, so routeSpeech() can't resolve it and the agent's reply is
567
+ * silently dropped even though the inbound message proves the channel is
568
+ * reachable (item-3 redux, DM sub-case). Idempotent: a later authoritative
569
+ * channels/register or channels/changed overwrites this descriptor with the
570
+ * richer one; re-opening an already-open channel is a no-op.
571
+ */
572
+ ensureChannelRegistered(serverId: string, channelId: string, label?: string): void {
573
+ const existing = this.findChannelEntry(channelId);
574
+ if (existing) {
575
+ if (!existing.open) {
576
+ existing.open = true;
577
+ this.emitTraceFn({ type: 'mcpl:channel-opened', serverId: existing.serverId, channelId });
578
+ }
579
+ return;
580
+ }
581
+
582
+ const key = `${serverId}:${channelId}`;
583
+ this.channels.set(key, {
584
+ serverId,
585
+ descriptor: {
586
+ id: channelId,
587
+ type: serverId,
588
+ label: label ?? channelId,
589
+ direction: 'bidirectional',
590
+ metadata: { lazyRegistered: true },
591
+ },
592
+ open: true,
593
+ });
594
+ this.emitTraceFn({
595
+ type: 'mcpl:channel-lazy-registered',
596
+ serverId,
597
+ channelId,
598
+ label: label ?? channelId,
599
+ });
600
+ }
601
+
528
602
  // ==========================================================================
529
603
  // Typing Indicator Management
530
604
  // ==========================================================================
@@ -700,15 +774,28 @@ export class ChannelRegistry {
700
774
  *
701
775
  * Returns undefined if no channels are active.
702
776
  */
703
- buildChannelContext(): ChannelContext | undefined {
777
+ buildChannelContext(agentName?: string): ChannelContext | undefined {
704
778
  const openChannels = this.getOpenChannels();
705
- if (openChannels.length === 0 && !this.defaultPublishChannel) {
779
+
780
+ // Resolve the outbound locus the SAME way routeSpeech does, so the agent is
781
+ // told the channel its speech will actually land in: a conversation fork's
782
+ // home channel, else this turn's triggering channel (single trunk agent —
783
+ // item-3 redux), else the global default (heartbeats). Without this, a fork
784
+ // was advertised the global locus but published somewhere else; and a trunk
785
+ // agent was told the wrong channel under concurrency.
786
+ const home = agentName ? this.homeChannelResolver?.(agentName) : undefined;
787
+ const active = agentName ? this.activeChannelResolver?.(agentName) : undefined;
788
+ const outgoing = home ?? active ?? this.defaultPublishChannel;
789
+
790
+ if (openChannels.length === 0 && !outgoing) {
706
791
  return undefined;
707
792
  }
708
793
 
709
794
  const context: ChannelContext = {};
710
795
 
711
- // Incoming: from defaultPublishChannel if set
796
+ // Incoming: the most-recent inbound message (what the agent is replying to).
797
+ // Left process-global — per-channel inbound tracking (the right messageId for
798
+ // a fork's own channel) is a separate concern from the outbound routing fix.
712
799
  if (this.defaultPublishChannel && this.defaultPublishMessageId) {
713
800
  context.incoming = {
714
801
  channelId: this.defaultPublishChannel,
@@ -717,10 +804,10 @@ export class ChannelRegistry {
717
804
  };
718
805
  }
719
806
 
720
- // Default outgoing: same as incoming (reply to last channel)
721
- if (this.defaultPublishChannel) {
807
+ // Default outgoing: the resolved outbound locus (home channel for forks).
808
+ if (outgoing) {
722
809
  context.defaultOutgoing = {
723
- channelId: this.defaultPublishChannel,
810
+ channelId: outgoing,
724
811
  };
725
812
  }
726
813
 
@@ -736,6 +823,25 @@ export class ChannelRegistry {
736
823
  // Lifecycle
737
824
  // ==========================================================================
738
825
 
826
+ /**
827
+ * Remove all channel state belonging to a single server. Called by the
828
+ * framework when an MCPL server is disconnected at runtime, so a dead
829
+ * server's channels don't linger and route speech into the void.
830
+ */
831
+ removeServer(serverId: string): void {
832
+ for (const [key, entry] of this.channels) {
833
+ if (entry.serverId !== serverId) continue;
834
+ this.channels.delete(key);
835
+ this.stopTyping(entry.descriptor.id);
836
+ if (this.defaultPublishChannel === entry.descriptor.id) {
837
+ this.defaultPublishChannel = null;
838
+ this.defaultPublishMessageId = null;
839
+ this.defaultPublishThreadId = undefined;
840
+ }
841
+ }
842
+ this.subscriptionPolicies.delete(serverId);
843
+ }
844
+
739
845
  /**
740
846
  * Stop all typing intervals and clear all channel registrations.
741
847
  */
@@ -1034,9 +1140,23 @@ export class ChannelRegistry {
1034
1140
  * is no locus / the channel or its server can't be resolved (in which case
1035
1141
  * the speech simply stays in chronicle + module surfaces).
1036
1142
  */
1143
+ /** Resolve the outbound locus (fork HOME → this-turn's TRIGGERING channel →
1144
+ * process-global default). Public so a multi-segment caller can snapshot it
1145
+ * ONCE and pin every segment to it via routeSpeech's `overrideChannelId`. */
1146
+ resolveLocus(conversationId: string): string | null {
1147
+ const home = this.homeChannelResolver?.(conversationId);
1148
+ return home ?? this.activeChannelResolver?.(conversationId) ?? this.defaultPublishChannel ?? null;
1149
+ }
1150
+
1037
1151
  async routeSpeech(
1038
1152
  conversationId: string,
1039
1153
  text: string,
1154
+ /** Pin the outbound locus, skipping resolution. Callers delivering MULTIPLE
1155
+ * segments of ONE turn snapshot resolveLocus() once and pass it here, so a
1156
+ * new turn starting mid-dispatch (segments run after the agent is idle,
1157
+ * PR #32) can't overwrite the per-agent triggering channel between segments
1158
+ * and split one reply across channels. */
1159
+ overrideChannelId?: string | null,
1040
1160
  ): Promise<{ delivered: boolean; channelId: string } | null> {
1041
1161
  // Surface a routing failure: emit a trace AND notify the host (which drops
1042
1162
  // a `[discord-send-failed]` marker into chronicle) so the agent learns her
@@ -1053,9 +1173,24 @@ export class ChannelRegistry {
1053
1173
  return null;
1054
1174
  };
1055
1175
 
1056
- const channelId = this.defaultPublishChannel;
1176
+ // Resolve the outbound locus, in precedence order:
1177
+ // 1. fork HOME channel — a conversation fork always replies in the channel
1178
+ // it was spawned to serve (item 3, forks).
1179
+ // 2. this turn's TRIGGERING channel — a single trunk agent (connectome-
1180
+ // host's only mode) replies in the channel whose message woke THIS
1181
+ // inference, so a concurrent inbound elsewhere can't hijack the reply
1182
+ // (item-3 redux, trunk agents). Also carries DM channels, which arrive
1183
+ // as push/events and never touch `defaultPublishChannel`.
1184
+ // 3. the process-global `defaultPublishChannel` — last resort for turns
1185
+ // with no triggering channel (heartbeats, timers).
1186
+ // Using the global for a fork or a concurrent trunk turn is the item-3 bug:
1187
+ // it tracks the most-recent inbound across ALL channels, so a reply lands
1188
+ // wherever a message last happened to arrive rather than where it belongs.
1189
+ const channelId = overrideChannelId ?? this.resolveLocus(conversationId);
1057
1190
  if (!channelId) {
1058
- return fail(null, 'no locus (defaultPublishChannel null)');
1191
+ // Reached only when the agent has no home, no active triggering channel,
1192
+ // AND no global inbound was ever seen.
1193
+ return fail(null, 'no locus (no home/active channel, defaultPublishChannel null)');
1059
1194
  }
1060
1195
 
1061
1196
  const entry = this.findChannelEntry(channelId);
@@ -368,6 +368,22 @@ export class CheckpointManager {
368
368
  return null;
369
369
  }
370
370
 
371
+ /**
372
+ * Get the server's host-managed (hostState) feature set. Host-managed state is
373
+ * single-valued by protocol (one unkeyed `state` per tool call), so there is at
374
+ * most one such set — return it so its state is threaded correctly even when the
375
+ * server also declares server-managed (rollback) sets. Returns null if none.
376
+ */
377
+ getHostManagedFeatureSet(serverId: string): string | null {
378
+ const prefix = `${serverId}:`;
379
+ for (const [key, tree] of this.trees) {
380
+ if (key.startsWith(prefix) && tree.hostState) {
381
+ return key.slice(prefix.length);
382
+ }
383
+ }
384
+ return null;
385
+ }
386
+
371
387
  // ==========================================================================
372
388
  // Rollback
373
389
  // ==========================================================================
@@ -171,12 +171,16 @@ export class PushHandler {
171
171
  const reason = err instanceof McplFeatureSetError
172
172
  ? err.message
173
173
  : 'Feature set validation failed';
174
+ // Loud rejection — a rejected push event is an agent that silently
175
+ // never hears the message. (2026-07-09 diagnosability pass.)
176
+ console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=${reason}`);
174
177
  responder?.respond({ accepted: false, reason });
175
178
  return;
176
179
  }
177
180
 
178
181
  // 2. Deduplicate by eventId
179
182
  if (this.dedup.checkAndAdd(params.eventId)) {
183
+ console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=duplicate`);
180
184
  responder?.respond({ accepted: false, reason: 'duplicate' });
181
185
  return;
182
186
  }
@@ -197,6 +201,7 @@ export class PushHandler {
197
201
  eventId: params.eventId,
198
202
  eventType: 'mcpl:push-event',
199
203
  ...(params.origin ?? {}),
204
+ ...(params.tags ? { tags: params.tags } : {}),
200
205
  };
201
206
  triggerInference = this.shouldTriggerInference(textContent, metadata);
202
207
  }