@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.
- package/dist/src/agent.d.ts +4 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +15 -0
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +243 -0
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1378 -114
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +64 -2
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +212 -12
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/gate-script-worker.d.ts +2 -0
- package/dist/src/gate/gate-script-worker.d.ts.map +1 -0
- package/dist/src/gate/gate-script-worker.js +69 -0
- package/dist/src/gate/gate-script-worker.js.map +1 -0
- package/dist/src/gate/gate-script.d.ts +44 -0
- package/dist/src/gate/gate-script.d.ts.map +1 -0
- package/dist/src/gate/gate-script.js +164 -0
- package/dist/src/gate/gate-script.js.map +1 -0
- package/dist/src/gate/types.d.ts +18 -3
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +60 -2
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +114 -9
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +7 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +15 -0
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +5 -0
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +64 -30
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +247 -210
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +98 -0
- package/dist/src/mcpl/transport.d.ts.map +1 -0
- package/dist/src/mcpl/transport.js +281 -0
- package/dist/src/mcpl/transport.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +59 -3
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +17 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +101 -6
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/prose-segments.d.ts +18 -0
- package/dist/src/prose-segments.d.ts.map +1 -0
- package/dist/src/prose-segments.js +39 -0
- package/dist/src/prose-segments.js.map +1 -0
- package/dist/src/runtime/liveness-watchdog-worker.d.ts +2 -0
- package/dist/src/runtime/liveness-watchdog-worker.d.ts.map +1 -0
- package/dist/src/runtime/liveness-watchdog-worker.js +57 -0
- package/dist/src/runtime/liveness-watchdog-worker.js.map +1 -0
- package/dist/src/runtime/liveness-watchdog.d.ts +36 -0
- package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -0
- package/dist/src/runtime/liveness-watchdog.js +106 -0
- package/dist/src/runtime/liveness-watchdog.js.map +1 -0
- package/dist/src/types/agent.d.ts +47 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +14 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +58 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +42 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/agent-cache-ttl.test.d.ts +2 -0
- package/dist/test/agent-cache-ttl.test.d.ts.map +1 -0
- package/dist/test/agent-cache-ttl.test.js +25 -0
- package/dist/test/agent-cache-ttl.test.js.map +1 -0
- package/dist/test/channel-registry-routing.test.js +108 -1
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/checkpoint-attribution.test.d.ts +2 -0
- package/dist/test/checkpoint-attribution.test.d.ts.map +1 -0
- package/dist/test/checkpoint-attribution.test.js +40 -0
- package/dist/test/checkpoint-attribution.test.js.map +1 -0
- package/dist/test/conversation-fork-injection-scoping.test.d.ts +2 -0
- package/dist/test/conversation-fork-injection-scoping.test.d.ts.map +1 -0
- package/dist/test/conversation-fork-injection-scoping.test.js +184 -0
- package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -0
- package/dist/test/event-gate-policy-mutation.test.d.ts +2 -0
- package/dist/test/event-gate-policy-mutation.test.d.ts.map +1 -0
- package/dist/test/event-gate-policy-mutation.test.js +137 -0
- package/dist/test/event-gate-policy-mutation.test.js.map +1 -0
- package/dist/test/event-gate-script.test.d.ts +2 -0
- package/dist/test/event-gate-script.test.d.ts.map +1 -0
- package/dist/test/event-gate-script.test.js +95 -0
- package/dist/test/event-gate-script.test.js.map +1 -0
- package/dist/test/event-gate-tags.test.d.ts +2 -0
- package/dist/test/event-gate-tags.test.d.ts.map +1 -0
- package/dist/test/event-gate-tags.test.js +76 -0
- package/dist/test/event-gate-tags.test.js.map +1 -0
- package/dist/test/gather-context-timeout.test.d.ts +2 -0
- package/dist/test/gather-context-timeout.test.d.ts.map +1 -0
- package/dist/test/gather-context-timeout.test.js +120 -0
- package/dist/test/gather-context-timeout.test.js.map +1 -0
- package/dist/test/inference-exhaustion-rewind.test.d.ts +2 -0
- package/dist/test/inference-exhaustion-rewind.test.d.ts.map +1 -0
- package/dist/test/inference-exhaustion-rewind.test.js +181 -0
- package/dist/test/inference-exhaustion-rewind.test.js.map +1 -0
- package/dist/test/inference-failure-observability.test.js +4 -0
- package/dist/test/inference-failure-observability.test.js.map +1 -1
- package/dist/test/liveness-watchdog.test.d.ts +2 -0
- package/dist/test/liveness-watchdog.test.d.ts.map +1 -0
- package/dist/test/liveness-watchdog.test.js +78 -0
- package/dist/test/liveness-watchdog.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-events.test.d.ts +2 -0
- package/dist/test/mcpl-reconnect-events.test.d.ts.map +1 -0
- package/dist/test/mcpl-reconnect-events.test.js +119 -0
- package/dist/test/mcpl-reconnect-events.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts +2 -0
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts.map +1 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js +181 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -0
- package/dist/test/mcpl-request-timeout.test.d.ts +2 -0
- package/dist/test/mcpl-request-timeout.test.d.ts.map +1 -0
- package/dist/test/mcpl-request-timeout.test.js +123 -0
- package/dist/test/mcpl-request-timeout.test.js.map +1 -0
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts +7 -0
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts.map +1 -0
- package/dist/test/mcpl-runtime-lifecycle.test.js +142 -0
- package/dist/test/mcpl-runtime-lifecycle.test.js.map +1 -0
- package/dist/test/mcpl-ws-transport.test.d.ts +2 -0
- package/dist/test/mcpl-ws-transport.test.d.ts.map +1 -0
- package/dist/test/mcpl-ws-transport.test.js +155 -0
- package/dist/test/mcpl-ws-transport.test.js.map +1 -0
- package/dist/test/prose-segments.test.d.ts +2 -0
- package/dist/test/prose-segments.test.d.ts.map +1 -0
- package/dist/test/prose-segments.test.js +52 -0
- package/dist/test/prose-segments.test.js.map +1 -0
- package/dist/test/refusal-rewind.test.d.ts +2 -0
- package/dist/test/refusal-rewind.test.d.ts.map +1 -0
- package/dist/test/refusal-rewind.test.js +108 -0
- package/dist/test/refusal-rewind.test.js.map +1 -0
- package/dist/test/state-scaling-e2e.test.d.ts +22 -0
- package/dist/test/state-scaling-e2e.test.d.ts.map +1 -0
- package/dist/test/state-scaling-e2e.test.js +200 -0
- package/dist/test/state-scaling-e2e.test.js.map +1 -0
- package/dist/test/state-scaling.test.d.ts +12 -0
- package/dist/test/state-scaling.test.d.ts.map +1 -0
- package/dist/test/state-scaling.test.js +243 -0
- package/dist/test/state-scaling.test.js.map +1 -0
- package/dist/test/tool-result-content.test.d.ts +2 -0
- package/dist/test/tool-result-content.test.d.ts.map +1 -0
- package/dist/test/tool-result-content.test.js +37 -0
- package/dist/test/tool-result-content.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.d.ts +19 -0
- package/dist/test/trunk-channel-routing.test.d.ts.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +175 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/agent.ts +16 -0
- package/src/framework.ts +1576 -121
- package/src/gate/event-gate.ts +209 -12
- package/src/gate/gate-script-worker.ts +73 -0
- package/src/gate/gate-script.ts +180 -0
- package/src/gate/types.ts +19 -3
- package/src/index.ts +10 -0
- package/src/mcpl/channel-registry.ts +143 -8
- package/src/mcpl/checkpoint-manager.ts +16 -0
- package/src/mcpl/push-handler.ts +5 -0
- package/src/mcpl/server-connection.ts +262 -227
- package/src/mcpl/transport.ts +307 -0
- package/src/mcpl/types.ts +68 -3
- package/src/module-registry.ts +126 -6
- package/src/prose-segments.ts +42 -0
- package/src/runtime/liveness-watchdog-worker.ts +66 -0
- package/src/runtime/liveness-watchdog.ts +122 -0
- package/src/types/agent.ts +48 -0
- package/src/types/framework.ts +15 -0
- package/src/types/module.ts +65 -0
- 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
|
-
|
|
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:
|
|
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:
|
|
721
|
-
if (
|
|
807
|
+
// Default outgoing: the resolved outbound locus (home channel for forks).
|
|
808
|
+
if (outgoing) {
|
|
722
809
|
context.defaultOutgoing = {
|
|
723
|
-
channelId:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// ==========================================================================
|
package/src/mcpl/push-handler.ts
CHANGED
|
@@ -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
|
}
|