@animalabs/agent-framework 0.5.7 → 0.6.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 (116) hide show
  1. package/dist/src/framework.d.ts +67 -0
  2. package/dist/src/framework.d.ts.map +1 -1
  3. package/dist/src/framework.js +469 -26
  4. package/dist/src/framework.js.map +1 -1
  5. package/dist/src/gate/event-gate.d.ts +44 -1
  6. package/dist/src/gate/event-gate.d.ts.map +1 -1
  7. package/dist/src/gate/event-gate.js +152 -8
  8. package/dist/src/gate/event-gate.js.map +1 -1
  9. package/dist/src/gate/gate-script-worker.d.ts +2 -0
  10. package/dist/src/gate/gate-script-worker.d.ts.map +1 -0
  11. package/dist/src/gate/gate-script-worker.js +69 -0
  12. package/dist/src/gate/gate-script-worker.js.map +1 -0
  13. package/dist/src/gate/gate-script.d.ts +44 -0
  14. package/dist/src/gate/gate-script.d.ts.map +1 -0
  15. package/dist/src/gate/gate-script.js +164 -0
  16. package/dist/src/gate/gate-script.js.map +1 -0
  17. package/dist/src/gate/types.d.ts +18 -3
  18. package/dist/src/gate/types.d.ts.map +1 -1
  19. package/dist/src/mcpl/channel-registry.d.ts +43 -1
  20. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  21. package/dist/src/mcpl/channel-registry.js +82 -8
  22. package/dist/src/mcpl/channel-registry.js.map +1 -1
  23. package/dist/src/mcpl/checkpoint-manager.d.ts +7 -0
  24. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  25. package/dist/src/mcpl/checkpoint-manager.js +15 -0
  26. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  27. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  28. package/dist/src/mcpl/push-handler.js +1 -0
  29. package/dist/src/mcpl/push-handler.js.map +1 -1
  30. package/dist/src/mcpl/server-connection.d.ts +35 -30
  31. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  32. package/dist/src/mcpl/server-connection.js +145 -206
  33. package/dist/src/mcpl/server-connection.js.map +1 -1
  34. package/dist/src/mcpl/transport.d.ts +98 -0
  35. package/dist/src/mcpl/transport.d.ts.map +1 -0
  36. package/dist/src/mcpl/transport.js +281 -0
  37. package/dist/src/mcpl/transport.js.map +1 -0
  38. package/dist/src/mcpl/types.d.ts +42 -2
  39. package/dist/src/mcpl/types.d.ts.map +1 -1
  40. package/dist/src/mcpl/types.js.map +1 -1
  41. package/dist/src/module-registry.d.ts +2 -0
  42. package/dist/src/module-registry.d.ts.map +1 -1
  43. package/dist/src/module-registry.js +9 -2
  44. package/dist/src/module-registry.js.map +1 -1
  45. package/dist/src/prose-segments.d.ts +18 -0
  46. package/dist/src/prose-segments.d.ts.map +1 -0
  47. package/dist/src/prose-segments.js +39 -0
  48. package/dist/src/prose-segments.js.map +1 -0
  49. package/dist/src/runtime/liveness-watchdog-worker.d.ts +2 -0
  50. package/dist/src/runtime/liveness-watchdog-worker.d.ts.map +1 -0
  51. package/dist/src/runtime/liveness-watchdog-worker.js +57 -0
  52. package/dist/src/runtime/liveness-watchdog-worker.js.map +1 -0
  53. package/dist/src/runtime/liveness-watchdog.d.ts +36 -0
  54. package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -0
  55. package/dist/src/runtime/liveness-watchdog.js +106 -0
  56. package/dist/src/runtime/liveness-watchdog.js.map +1 -0
  57. package/dist/src/types/agent.d.ts +10 -0
  58. package/dist/src/types/agent.d.ts.map +1 -1
  59. package/dist/src/types/framework.d.ts +14 -0
  60. package/dist/src/types/framework.d.ts.map +1 -1
  61. package/dist/src/types/module.d.ts +12 -0
  62. package/dist/src/types/module.d.ts.map +1 -1
  63. package/dist/test/channel-registry-routing.test.js +108 -1
  64. package/dist/test/channel-registry-routing.test.js.map +1 -1
  65. package/dist/test/checkpoint-attribution.test.d.ts +2 -0
  66. package/dist/test/checkpoint-attribution.test.d.ts.map +1 -0
  67. package/dist/test/checkpoint-attribution.test.js +40 -0
  68. package/dist/test/checkpoint-attribution.test.js.map +1 -0
  69. package/dist/test/event-gate-policy-mutation.test.d.ts +2 -0
  70. package/dist/test/event-gate-policy-mutation.test.d.ts.map +1 -0
  71. package/dist/test/event-gate-policy-mutation.test.js +137 -0
  72. package/dist/test/event-gate-policy-mutation.test.js.map +1 -0
  73. package/dist/test/event-gate-script.test.d.ts +2 -0
  74. package/dist/test/event-gate-script.test.d.ts.map +1 -0
  75. package/dist/test/event-gate-script.test.js +95 -0
  76. package/dist/test/event-gate-script.test.js.map +1 -0
  77. package/dist/test/event-gate-tags.test.d.ts +2 -0
  78. package/dist/test/event-gate-tags.test.d.ts.map +1 -0
  79. package/dist/test/event-gate-tags.test.js +76 -0
  80. package/dist/test/event-gate-tags.test.js.map +1 -0
  81. package/dist/test/liveness-watchdog.test.d.ts +2 -0
  82. package/dist/test/liveness-watchdog.test.d.ts.map +1 -0
  83. package/dist/test/liveness-watchdog.test.js +78 -0
  84. package/dist/test/liveness-watchdog.test.js.map +1 -0
  85. package/dist/test/mcpl-ws-transport.test.d.ts +2 -0
  86. package/dist/test/mcpl-ws-transport.test.d.ts.map +1 -0
  87. package/dist/test/mcpl-ws-transport.test.js +155 -0
  88. package/dist/test/mcpl-ws-transport.test.js.map +1 -0
  89. package/dist/test/prose-segments.test.d.ts +2 -0
  90. package/dist/test/prose-segments.test.d.ts.map +1 -0
  91. package/dist/test/prose-segments.test.js +52 -0
  92. package/dist/test/prose-segments.test.js.map +1 -0
  93. package/dist/test/trunk-channel-routing.test.d.ts +19 -0
  94. package/dist/test/trunk-channel-routing.test.d.ts.map +1 -0
  95. package/dist/test/trunk-channel-routing.test.js +175 -0
  96. package/dist/test/trunk-channel-routing.test.js.map +1 -0
  97. package/dist/tsconfig.tsbuildinfo +1 -1
  98. package/package.json +2 -2
  99. package/src/framework.ts +509 -24
  100. package/src/gate/event-gate.ts +153 -8
  101. package/src/gate/gate-script-worker.ts +73 -0
  102. package/src/gate/gate-script.ts +180 -0
  103. package/src/gate/types.ts +19 -3
  104. package/src/mcpl/channel-registry.ts +112 -8
  105. package/src/mcpl/checkpoint-manager.ts +16 -0
  106. package/src/mcpl/push-handler.ts +1 -0
  107. package/src/mcpl/server-connection.ts +153 -224
  108. package/src/mcpl/transport.ts +307 -0
  109. package/src/mcpl/types.ts +49 -2
  110. package/src/module-registry.ts +13 -2
  111. package/src/prose-segments.ts +42 -0
  112. package/src/runtime/liveness-watchdog-worker.ts +66 -0
  113. package/src/runtime/liveness-watchdog.ts +122 -0
  114. package/src/types/agent.ts +10 -0
  115. package/src/types/framework.ts +15 -0
  116. package/src/types/module.ts +13 -0
@@ -0,0 +1,155 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { WebSocketServer } from 'ws';
4
+ import { McplServerConnection } from '../src/mcpl/server-connection.js';
5
+ import { buildWebSocketUrl, isWebSocketTransport } from '../src/mcpl/transport.js';
6
+ const HOST_CAPS = { version: '0.4', pushEvents: true, featureSets: true };
7
+ /**
8
+ * A minimal in-process MCPL server over WebSocket: answers the initialize
9
+ * handshake, tools/list, and an `echo` tools/call. One JSON-RPC message per WS
10
+ * frame (as McplServerConnection sends). `onSocket` lets a test grab the raw
11
+ * socket to simulate a mid-session drop.
12
+ */
13
+ function startMockMcplWsServer(onSocket) {
14
+ const wss = new WebSocketServer({ port: 0, host: '127.0.0.1' });
15
+ wss.on('connection', (ws) => {
16
+ onSocket?.(ws);
17
+ ws.on('message', (data) => {
18
+ let msg;
19
+ try {
20
+ msg = JSON.parse(String(data));
21
+ }
22
+ catch {
23
+ return;
24
+ }
25
+ const reply = (result) => ws.send(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result }));
26
+ switch (msg.method) {
27
+ case 'initialize':
28
+ reply({ capabilities: { experimental: { mcpl: { version: '0.4', pushEvents: true } } } });
29
+ break;
30
+ case 'notifications/initialized':
31
+ break; // notification, no reply
32
+ case 'tools/list':
33
+ reply({ tools: [{ name: 'echo', description: 'Echo text back', inputSchema: { type: 'object', properties: { text: { type: 'string' } } } }] });
34
+ break;
35
+ case 'tools/call': {
36
+ const text = String(msg.params?.arguments?.text ?? '');
37
+ reply({ content: [{ type: 'text', text }] });
38
+ break;
39
+ }
40
+ default:
41
+ break;
42
+ }
43
+ });
44
+ });
45
+ return wss;
46
+ }
47
+ function wsUrl(wss) {
48
+ const port = wss.address().port;
49
+ return `ws://127.0.0.1:${port}/mcpl`;
50
+ }
51
+ function waitListening(wss) {
52
+ return new Promise((resolve) => (wss.address() ? resolve() : wss.once('listening', () => resolve())));
53
+ }
54
+ test('buildWebSocketUrl validates protocol and appends the token as a query param', () => {
55
+ assert.equal(buildWebSocketUrl({ id: 's', url: 'wss://example.com/mcpl' }), 'wss://example.com/mcpl');
56
+ assert.equal(buildWebSocketUrl({ id: 's', url: 'wss://example.com/mcpl', token: 'sekret' }), 'wss://example.com/mcpl?token=sekret');
57
+ // Preserves an existing query.
58
+ assert.equal(buildWebSocketUrl({ id: 's', url: 'wss://example.com/mcpl?v=2', token: 'abc' }), 'wss://example.com/mcpl?v=2&token=abc');
59
+ assert.throws(() => buildWebSocketUrl({ id: 's', url: 'http://example.com' }), /ws:\/\/ or wss:\/\//);
60
+ assert.throws(() => buildWebSocketUrl({ id: 's', command: 'x' }), /requires "url"/);
61
+ });
62
+ test('isWebSocketTransport selects the transport from config shape', () => {
63
+ assert.equal(isWebSocketTransport({ id: 's', url: 'wss://x/y' }), true);
64
+ assert.equal(isWebSocketTransport({ id: 's', url: 'wss://x/y', transport: 'websocket' }), true);
65
+ assert.equal(isWebSocketTransport({ id: 's', command: 'node' }), false);
66
+ // Explicit transport wins over shape.
67
+ assert.equal(isWebSocketTransport({ id: 's', command: 'node', transport: 'stdio' }), false);
68
+ assert.equal(isWebSocketTransport({ id: 's', url: 'wss://x', command: 'node' }), false); // url+command → stdio unless transport says otherwise
69
+ });
70
+ test('connects over WebSocket, negotiates capabilities, lists tools, and round-trips a call', async () => {
71
+ const wss = startMockMcplWsServer();
72
+ await waitListening(wss);
73
+ try {
74
+ const conn = await McplServerConnection.connect({ id: 'editor', url: wsUrl(wss) }, HOST_CAPS);
75
+ conn.ready();
76
+ // Handshake negotiated MCPL capabilities over WS.
77
+ assert.ok(conn.capabilities, 'expected MCPL capabilities from the handshake');
78
+ assert.equal(conn.capabilities?.pushEvents, true);
79
+ // tools/list round-trips.
80
+ const list = await conn.sendToolsList();
81
+ assert.deepEqual(list.tools.map((t) => t.name), ['echo']);
82
+ // tools/call round-trips.
83
+ const result = await conn.sendToolsCall('echo', { text: 'hello over wss' });
84
+ const block = result.content[0];
85
+ assert.equal(block.text, 'hello over wss');
86
+ await conn.close();
87
+ }
88
+ finally {
89
+ wss.close();
90
+ }
91
+ });
92
+ test('a failed WebSocket dial rejects with a clear error (no reconnect)', async () => {
93
+ // Nothing is listening on this port.
94
+ await assert.rejects(McplServerConnection.connect({ id: 'dead', url: 'ws://127.0.0.1:1/mcpl' }, HOST_CAPS), /WebSocket connect (failed|timed out)/);
95
+ });
96
+ // A tiny stdio MCPL server: reads NDJSON on stdin, answers the same three
97
+ // methods on stdout, and writes one line to stderr on boot (to prove stderr
98
+ // still surfaces after the transport refactor). Kept inline so the test is
99
+ // self-contained.
100
+ const STDIO_SERVER = `
101
+ let buf = '';
102
+ process.stderr.write('mock stdio server up\\n');
103
+ process.stdin.on('data', (c) => {
104
+ buf += c.toString('utf8');
105
+ let i;
106
+ while ((i = buf.indexOf('\\n')) >= 0) {
107
+ const line = buf.slice(0, i); buf = buf.slice(i + 1);
108
+ if (!line.trim()) continue;
109
+ let m; try { m = JSON.parse(line); } catch { continue; }
110
+ const reply = (result) => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: m.id, result }) + '\\n');
111
+ if (m.method === 'initialize') reply({ capabilities: { experimental: { mcpl: { version: '0.4', pushEvents: true } } } });
112
+ else if (m.method === 'tools/list') reply({ tools: [{ name: 'echo', description: 'e', inputSchema: { type: 'object' } }] });
113
+ else if (m.method === 'tools/call') reply({ content: [{ type: 'text', text: String((m.params && m.params.arguments && m.params.arguments.text) || '') }] });
114
+ }
115
+ });
116
+ `;
117
+ test('stdio transport still handshakes, lists tools, round-trips a call, and surfaces stderr', async () => {
118
+ const conn = await McplServerConnection.connect({ id: 'stdiosrv', command: process.execPath, args: ['-e', STDIO_SERVER] }, HOST_CAPS);
119
+ const stderrLines = [];
120
+ conn.on('stderr', (p) => stderrLines.push(p.line));
121
+ conn.ready();
122
+ assert.ok(conn.capabilities, 'stdio handshake should negotiate capabilities');
123
+ const list = await conn.sendToolsList();
124
+ assert.deepEqual(list.tools.map((t) => t.name), ['echo']);
125
+ const result = await conn.sendToolsCall('echo', { text: 'over stdio' });
126
+ const block = result.content[0];
127
+ assert.equal(block.text, 'over stdio');
128
+ // stderr forwarding preserved (buffered pre-listener, flushed on subscribe).
129
+ await new Promise((r) => setTimeout(r, 20));
130
+ assert.ok(stderrLines.some((l) => l.includes('mock stdio server up')), 'stderr line should surface');
131
+ await conn.close();
132
+ });
133
+ test('reconnect: an unexpected socket drop triggers a background reconnect', async () => {
134
+ let sockets = [];
135
+ const wss = startMockMcplWsServer((ws) => sockets.push(ws));
136
+ await waitListening(wss);
137
+ try {
138
+ const conn = await McplServerConnection.connect({ id: 'editor', url: wsUrl(wss), reconnect: true, reconnectIntervalMs: 100 }, HOST_CAPS);
139
+ conn.ready();
140
+ const reconnected = new Promise((resolve) => conn.once('reconnect', () => resolve()));
141
+ // Force-drop the server side of the current socket.
142
+ assert.equal(sockets.length, 1);
143
+ sockets[0].terminate();
144
+ await reconnected; // background reconnect must fire
145
+ // The new socket works: a call round-trips post-reconnect.
146
+ const result = await conn.sendToolsCall('echo', { text: 'after reconnect' });
147
+ const block = result.content[0];
148
+ assert.equal(block.text, 'after reconnect');
149
+ await conn.close();
150
+ }
151
+ finally {
152
+ wss.close();
153
+ }
154
+ });
155
+ //# sourceMappingURL=mcpl-ws-transport.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpl-ws-transport.test.js","sourceRoot":"","sources":["../../test/mcpl-ws-transport.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=prose-segments.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prose-segments.test.d.ts","sourceRoot":"","sources":["../../test/prose-segments.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,52 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { splitProseSegments } from '../src/prose-segments.js';
4
+ const textBlock = (text) => ({ type: 'text', text });
5
+ const toolUse = (id) => ({ type: 'tool_use', id, name: 'workspace--write', input: {} });
6
+ const toolResult = (id) => ({ type: 'tool_result', toolUseId: id, content: 'done', isError: false });
7
+ const thinking = () => ({ type: 'thinking', thinking: '', signature: 'x' });
8
+ test('item 4: prose before a tool_use is not dropped or merged — it becomes its own ordered segment', () => {
9
+ // Shape of the real captured turn (docs/reports/item4-failing-transcript.jsonl),
10
+ // as the membrane accumulates it into response.content across tool rounds:
11
+ // [text FIRST, tool_use, tool_result, thinking, tool_use, tool_result, text SECOND]
12
+ const content = [
13
+ textBlock('FIRST_MESSAGE_BEFORE_TOOL.'),
14
+ toolUse('t1'),
15
+ toolResult('t1'),
16
+ thinking(),
17
+ toolUse('t2'),
18
+ toolResult('t2'),
19
+ textBlock('SECOND_MESSAGE_AFTER_TOOL.'),
20
+ ];
21
+ const segments = splitProseSegments(content);
22
+ // Old behaviour joined all text into ONE trailing post
23
+ // ("FIRST_MESSAGE_BEFORE_TOOL.\nSECOND_MESSAGE_AFTER_TOOL."); the fix keeps
24
+ // them as two separate messages IN ORDER.
25
+ assert.deepEqual(segments, ['FIRST_MESSAGE_BEFORE_TOOL.', 'SECOND_MESSAGE_AFTER_TOOL.']);
26
+ });
27
+ test('three interleaved messages (msgA → [tool] → msgB → [tool] → msgC) all survive in order', () => {
28
+ const content = [
29
+ textBlock('A'),
30
+ toolUse('t1'), toolResult('t1'),
31
+ textBlock('B'),
32
+ toolUse('t2'), toolResult('t2'),
33
+ textBlock('C'),
34
+ ];
35
+ assert.deepEqual(splitProseSegments(content), ['A', 'B', 'C']);
36
+ });
37
+ test('contiguous text blocks merge into one segment; whitespace-only runs are dropped', () => {
38
+ const content = [
39
+ textBlock('line one'),
40
+ textBlock('line two'),
41
+ toolUse('t1'), toolResult('t1'),
42
+ textBlock(' '),
43
+ ];
44
+ assert.deepEqual(splitProseSegments(content), ['line one\nline two']);
45
+ });
46
+ test('a plain no-tool turn yields a single segment (unchanged behaviour)', () => {
47
+ assert.deepEqual(splitProseSegments([textBlock('just a reply')]), ['just a reply']);
48
+ });
49
+ test('a tool-only turn (no prose) yields no segments', () => {
50
+ assert.deepEqual(splitProseSegments([toolUse('t1'), toolResult('t1')]), []);
51
+ });
52
+ //# sourceMappingURL=prose-segments.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prose-segments.test.js","sourceRoot":"","sources":["../../test/prose-segments.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Item-3 redux: single-TRUNK output routing.
3
+ *
4
+ * These tests cover the FRAMEWORK-side plumbing that feeds the ChannelRegistry's
5
+ * `activeChannelResolver` (the routing DECISION itself is covered in
6
+ * channel-registry-routing.test.ts):
7
+ * - derivePushEventChannel() reconstructs the MCPL composite channel for a
8
+ * push event (Discord DMs arrive this way), preferring an explicit
9
+ * origin.mcplChannelId.
10
+ * - a channel-incoming turn records its triggering channel per-agent.
11
+ * - a DM push-event turn records the reconstructed DM channel per-agent.
12
+ * - a batched wake picks the MOST-RECENT triggering channel.
13
+ *
14
+ * connectome-host runs every agent as a single trunk (it never sets
15
+ * `conversations`), so no fork/home exists — the active triggering channel is
16
+ * the only thing that keeps a reply in the channel it is answering.
17
+ */
18
+ export {};
19
+ //# sourceMappingURL=trunk-channel-routing.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trunk-channel-routing.test.d.ts","sourceRoot":"","sources":["../../test/trunk-channel-routing.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Item-3 redux: single-TRUNK output routing.
3
+ *
4
+ * These tests cover the FRAMEWORK-side plumbing that feeds the ChannelRegistry's
5
+ * `activeChannelResolver` (the routing DECISION itself is covered in
6
+ * channel-registry-routing.test.ts):
7
+ * - derivePushEventChannel() reconstructs the MCPL composite channel for a
8
+ * push event (Discord DMs arrive this way), preferring an explicit
9
+ * origin.mcplChannelId.
10
+ * - a channel-incoming turn records its triggering channel per-agent.
11
+ * - a DM push-event turn records the reconstructed DM channel per-agent.
12
+ * - a batched wake picks the MOST-RECENT triggering channel.
13
+ *
14
+ * connectome-host runs every agent as a single trunk (it never sets
15
+ * `conversations`), so no fork/home exists — the active triggering channel is
16
+ * the only thing that keeps a reply in the channel it is answering.
17
+ */
18
+ import { describe, it, beforeEach, afterEach } from 'node:test';
19
+ import assert from 'node:assert/strict';
20
+ import { tmpdir } from 'node:os';
21
+ import { mkdtempSync, rmSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import { AgentFramework } from '../src/index.js';
24
+ import { MockMembrane, createMockResponse } from './helpers/mock-membrane.js';
25
+ /** Reach the private per-turn triggering-channel map + the pure channel-deriver. */
26
+ function internals(framework) {
27
+ return framework;
28
+ }
29
+ function channelIncoming(channelId, text) {
30
+ return {
31
+ type: 'mcpl:channel-incoming',
32
+ serverId: 'discord',
33
+ channelId,
34
+ messageId: `m-${Math.random().toString(36).slice(2)}`,
35
+ author: { id: 'U1', name: 'antra' },
36
+ content: [{ type: 'text', text }],
37
+ timestamp: new Date().toISOString(),
38
+ metadata: {},
39
+ triggerInference: true,
40
+ };
41
+ }
42
+ /** A Discord DM as discord-mcpl forwards it: a push/event whose origin carries
43
+ * the raw channel (guildId null) — no channels/incoming, no open channel. */
44
+ function dmPushEvent(rawChannelId, text) {
45
+ return {
46
+ type: 'mcpl:push-event',
47
+ serverId: 'discord',
48
+ featureSet: 'discord.messaging',
49
+ eventId: `discord_msg_${Math.random().toString(36).slice(2)}`,
50
+ content: [{ type: 'text', text }],
51
+ origin: {
52
+ source: 'discord',
53
+ channelId: rawChannelId,
54
+ guildId: null,
55
+ channelName: undefined,
56
+ isDM: true,
57
+ },
58
+ timestamp: new Date().toISOString(),
59
+ inferenceId: `inf-${Math.random().toString(36).slice(2)}`,
60
+ triggerInference: true,
61
+ };
62
+ }
63
+ describe('Trunk channel routing (item-3 redux)', () => {
64
+ let tempDir;
65
+ let membrane;
66
+ beforeEach(() => {
67
+ tempDir = mkdtempSync(join(tmpdir(), 'trunk-routing-test-'));
68
+ membrane = new MockMembrane();
69
+ });
70
+ afterEach(() => {
71
+ rmSync(tempDir, { recursive: true, force: true });
72
+ });
73
+ async function makeFramework() {
74
+ // NO `conversations` — this is connectome-host's single-trunk mode.
75
+ return AgentFramework.create({
76
+ storePath: join(tempDir, 'test.chronicle'),
77
+ membrane: membrane.asMembrane(),
78
+ agents: [{ name: 'scout', model: 'test-model', systemPrompt: 'You are scout.' }],
79
+ modules: [],
80
+ });
81
+ }
82
+ it('derivePushEventChannel prefers an explicit origin.mcplChannelId', async () => {
83
+ const framework = await makeFramework();
84
+ const got = internals(framework).derivePushEventChannel({
85
+ source: 'discord',
86
+ mcplChannelId: 'discord:dm:999',
87
+ channelName: 'DM with Antra',
88
+ channelId: '999',
89
+ });
90
+ assert.deepEqual(got, { channelId: 'discord:dm:999', label: 'DM with Antra' });
91
+ await framework.stop();
92
+ });
93
+ it('derivePushEventChannel reconstructs a Discord DM composite (guildId null -> dm)', async () => {
94
+ const framework = await makeFramework();
95
+ const got = internals(framework).derivePushEventChannel({
96
+ source: 'discord',
97
+ channelId: '42',
98
+ guildId: null,
99
+ isDM: true,
100
+ });
101
+ assert.deepEqual(got, { channelId: 'discord:dm:42', label: undefined });
102
+ await framework.stop();
103
+ });
104
+ it('derivePushEventChannel reconstructs a non-open guild channel composite', async () => {
105
+ const framework = await makeFramework();
106
+ const got = internals(framework).derivePushEventChannel({
107
+ source: 'discord',
108
+ channelId: 'C7',
109
+ guildId: 'G1',
110
+ });
111
+ assert.deepEqual(got, { channelId: 'discord:G1:C7', label: undefined });
112
+ await framework.stop();
113
+ });
114
+ it('derivePushEventChannel returns undefined for a channel-less push (heartbeat)', async () => {
115
+ const framework = await makeFramework();
116
+ const i = internals(framework);
117
+ assert.equal(i.derivePushEventChannel(undefined), undefined);
118
+ assert.equal(i.derivePushEventChannel({ source: 'heartbeat', kind: 'tick' }), undefined);
119
+ await framework.stop();
120
+ });
121
+ it('a channel-incoming trunk turn records its triggering channel', async () => {
122
+ membrane.pushResponse(createMockResponse([{ type: 'text', text: 'the date is ...' }]));
123
+ const framework = await makeFramework();
124
+ framework.pushEvent(channelIncoming('discord:guild:chanA', 'A: sleep && date'));
125
+ await framework.runUntilIdle();
126
+ assert.equal(membrane.calls.length, 1, 'the trunk should have run one turn');
127
+ assert.equal(internals(framework).activeTriggerChannels.get('scout'), 'discord:guild:chanA', 'the turn must be routed to the channel that triggered it');
128
+ await framework.stop();
129
+ });
130
+ it('a DM push-event turn records the reconstructed DM channel (item-3 redux DM sub-case)', async () => {
131
+ membrane.pushResponse(createMockResponse([{ type: 'text', text: 'hi in the DM' }]));
132
+ const framework = await makeFramework();
133
+ framework.pushEvent(dmPushEvent('42', 'hey scout, ping'));
134
+ await framework.runUntilIdle();
135
+ assert.equal(membrane.calls.length, 1, 'the trunk should wake for the DM');
136
+ assert.equal(internals(framework).activeTriggerChannels.get('scout'), 'discord:dm:42', 'the DM reply must route to the DM channel, not the global locus');
137
+ await framework.stop();
138
+ });
139
+ it('the triggering channel tracks the CURRENT turn, never a stale one', async () => {
140
+ const framework = await makeFramework();
141
+ const i = internals(framework);
142
+ // A channel-A turn sets the active channel... (push the response right before
143
+ // each turn: MockMembrane's stream consumes ALL queued responses at once.)
144
+ membrane.pushResponse(createMockResponse([{ type: 'text', text: 'ans A' }]));
145
+ framework.pushEvent(channelIncoming('discord:guild:chanA', 'A: hi'));
146
+ await framework.runUntilIdle();
147
+ assert.equal(i.activeTriggerChannels.get('scout'), 'discord:guild:chanA');
148
+ // ...then a DM turn OVERWRITES it — the next turn's reply must never inherit
149
+ // the previous turn's channel (the concurrency hazard this fix removes).
150
+ membrane.pushResponse(createMockResponse([{ type: 'text', text: 'ans DM' }]));
151
+ framework.pushEvent(dmPushEvent('42', 'now in a DM'));
152
+ await framework.runUntilIdle();
153
+ assert.equal(membrane.calls.length, 2, 'both turns should have run');
154
+ assert.equal(i.activeTriggerChannels.get('scout'), 'discord:dm:42', 'the map must reflect the CURRENT turn’s channel, not chanA');
155
+ await framework.stop();
156
+ });
157
+ it('startAgentStream clears the triggering channel for a no-channel (heartbeat) turn', async () => {
158
+ membrane.pushResponse(createMockResponse([{ type: 'text', text: 'tick' }]));
159
+ const framework = await makeFramework();
160
+ const i = internals(framework);
161
+ // Simulate a stale channel left by a prior turn, then run a heartbeat/timer
162
+ // turn (an InferenceRequest with no channelId). startAgentStream must clear
163
+ // the entry up front so routeSpeech falls back to the global locus rather
164
+ // than replaying the stale channel.
165
+ i.activeTriggerChannels.set('scout', 'discord:guild:stale');
166
+ const scout = framework.getAgent('scout');
167
+ await framework.startAgentStream(scout, {
168
+ agentName: 'scout', reason: 'heartbeat', source: 'timer', timestamp: Date.now(),
169
+ });
170
+ await framework.runUntilIdle();
171
+ assert.equal(i.activeTriggerChannels.has('scout'), false, 'a no-trigger turn must clear any stale triggering channel');
172
+ await framework.stop();
173
+ });
174
+ });
175
+ //# sourceMappingURL=trunk-channel-routing.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trunk-channel-routing.test.js","sourceRoot":"","sources":["../../test/trunk-channel-routing.test.ts"],"names":[],"mappings":"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"}