@copilotkit/channels-core 0.5.0 → 0.5.1-canary.1785633429

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 (82) hide show
  1. package/README.md +35 -14
  2. package/dist/action-registry.d.ts +15 -3
  3. package/dist/action-registry.d.ts.map +1 -1
  4. package/dist/action-registry.js +61 -9
  5. package/dist/action-registry.test.js +107 -2
  6. package/dist/action-store.d.ts +23 -0
  7. package/dist/action-store.d.ts.map +1 -1
  8. package/dist/action-store.js +11 -0
  9. package/dist/canonical-run-loop.test.js +6 -1
  10. package/dist/channel-identity.test.d.ts +2 -0
  11. package/dist/channel-identity.test.d.ts.map +1 -0
  12. package/dist/channel-identity.test.js +297 -0
  13. package/dist/channel-memory.test.d.ts +2 -0
  14. package/dist/channel-memory.test.d.ts.map +1 -0
  15. package/dist/channel-memory.test.js +199 -0
  16. package/dist/commands.d.ts +5 -3
  17. package/dist/commands.d.ts.map +1 -1
  18. package/dist/create-channel.d.ts +30 -56
  19. package/dist/create-channel.d.ts.map +1 -1
  20. package/dist/create-channel.foundations.test.js +30 -7
  21. package/dist/create-channel.js +209 -72
  22. package/dist/create-channel.test.js +293 -66
  23. package/dist/hitl-continuation.test.d.ts +2 -0
  24. package/dist/hitl-continuation.test.d.ts.map +1 -0
  25. package/dist/hitl-continuation.test.js +393 -0
  26. package/dist/identity.d.ts +51 -0
  27. package/dist/identity.d.ts.map +1 -0
  28. package/dist/identity.js +56 -0
  29. package/dist/identity.test.d.ts +2 -0
  30. package/dist/identity.test.d.ts.map +1 -0
  31. package/dist/identity.test.js +109 -0
  32. package/dist/index.d.ts +10 -5
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +5 -1
  35. package/dist/managed-v1-await-choice-guard.test.js +4 -1
  36. package/dist/memory.d.ts +23 -0
  37. package/dist/memory.d.ts.map +1 -0
  38. package/dist/memory.js +25 -0
  39. package/dist/modal-submit.test.js +21 -6
  40. package/dist/open-modal.test.js +16 -4
  41. package/dist/platform-adapter.d.ts +29 -6
  42. package/dist/platform-adapter.d.ts.map +1 -1
  43. package/dist/platform-adapter.test.js +2 -0
  44. package/dist/reactions.test.js +49 -11
  45. package/dist/run-loop.test.js +30 -5
  46. package/dist/sanitize-agent-events.test.js +2 -0
  47. package/dist/source-platform.test.js +24 -9
  48. package/dist/state/kv-action-store.d.ts.map +1 -1
  49. package/dist/state/kv-action-store.js +1 -0
  50. package/dist/state/kv-action-store.test.js +11 -0
  51. package/dist/state/memory-store.d.ts +1 -0
  52. package/dist/state/memory-store.d.ts.map +1 -1
  53. package/dist/state/memory-store.js +9 -0
  54. package/dist/state/state-store.d.ts +2 -0
  55. package/dist/state/state-store.d.ts.map +1 -1
  56. package/dist/telemetry/create-channel-telemetry.test.js +4 -2
  57. package/dist/telemetry/e2e-telemetry.test.js +1 -0
  58. package/dist/telemetry/install-id.test.js +5 -0
  59. package/dist/testing/fake-adapter.d.ts +26 -9
  60. package/dist/testing/fake-adapter.d.ts.map +1 -1
  61. package/dist/testing/fake-adapter.js +65 -21
  62. package/dist/testing/state-store-conformance.d.ts.map +1 -1
  63. package/dist/testing/state-store-conformance.js +16 -0
  64. package/dist/thread-capabilities.test.js +29 -9
  65. package/dist/thread-ephemeral.test.js +8 -5
  66. package/dist/thread-promise-contract.test.js +4 -0
  67. package/dist/thread-reactions.test.js +8 -2
  68. package/dist/thread.d.ts +50 -7
  69. package/dist/thread.d.ts.map +1 -1
  70. package/dist/thread.js +163 -17
  71. package/dist/thread.test.js +4 -0
  72. package/dist/tools.d.ts +5 -3
  73. package/dist/tools.d.ts.map +1 -1
  74. package/dist/tools.js +2 -1
  75. package/dist/transcripts.d.ts +6 -13
  76. package/dist/transcripts.d.ts.map +1 -1
  77. package/dist/transcripts.js +12 -12
  78. package/dist/transcripts.test.js +28 -29
  79. package/dist/welcome.test.d.ts +2 -0
  80. package/dist/welcome.test.d.ts.map +1 -0
  81. package/dist/welcome.test.js +53 -0
  82. package/package.json +5 -5
@@ -13,7 +13,10 @@ const tick = () => new Promise((r) => setTimeout(r, 0));
13
13
  * `ctx.action.value`.
14
14
  */
15
15
  const __handlerTypeGuards = () => {
16
- const channel = createChannel({ adapters: [new FakeAdapter()] });
16
+ const channel = createChannel({
17
+ identifyUser: "platform",
18
+ adapters: [new FakeAdapter()],
19
+ });
17
20
  channel.onInterrupt("ask", ({ payload }) => {
18
21
  payload.question.toUpperCase();
19
22
  // @ts-expect-error 'missing' is not on the payload type
@@ -112,13 +115,20 @@ function captureSessionAgents(fake) {
112
115
  describe("createChannel", () => {
113
116
  it("rejects activation when a message-capable adapter has no eligible handler", async () => {
114
117
  const fake = new FakeAdapter({ messageEvents: true });
115
- const channel = createChannel({ name: "support", adapters: [fake] });
118
+ const channel = createChannel({
119
+ identifyUser: "platform",
120
+ name: "support",
121
+ adapters: [fake],
122
+ });
116
123
  await expect(channel.ɵruntime.start()).rejects.toThrow('channel "support" must register onMention or onMessage');
117
124
  expect(fake.started).toBe(false);
118
125
  });
119
126
  it("routes an explicit mention only to onMention", async () => {
120
127
  const fake = new FakeAdapter();
121
- const channel = createChannel({ adapters: [fake] });
128
+ const channel = createChannel({
129
+ identifyUser: "platform",
130
+ adapters: [fake],
131
+ });
122
132
  const mentions = vi.fn();
123
133
  const messages = vi.fn();
124
134
  channel.onMention(mentions);
@@ -141,7 +151,10 @@ describe("createChannel", () => {
141
151
  });
142
152
  it("falls an explicit mention back to onMessage", async () => {
143
153
  const fake = new FakeAdapter();
144
- const channel = createChannel({ adapters: [fake] });
154
+ const channel = createChannel({
155
+ identifyUser: "platform",
156
+ adapters: [fake],
157
+ });
145
158
  const messages = vi.fn();
146
159
  channel.onMessage(messages);
147
160
  await channel.ɵruntime.start();
@@ -172,7 +185,10 @@ describe("createChannel", () => {
172
185
  });
173
186
  it("routes an ordinary message only to onMessage", async () => {
174
187
  const fake = new FakeAdapter();
175
- const channel = createChannel({ adapters: [fake] });
188
+ const channel = createChannel({
189
+ identifyUser: "platform",
190
+ adapters: [fake],
191
+ });
176
192
  const mentions = vi.fn();
177
193
  const messages = vi.fn();
178
194
  channel.onMention(mentions);
@@ -196,7 +212,11 @@ describe("createChannel", () => {
196
212
  it("routes a mention to a handler that posts UI", async () => {
197
213
  const fake = new FakeAdapter();
198
214
  const agent = new FakeAgent();
199
- const channel = createChannel({ adapters: [fake], agent: () => agent });
215
+ const channel = createChannel({
216
+ identifyUser: "platform",
217
+ adapters: [fake],
218
+ agent: () => agent,
219
+ });
200
220
  channel.onMention(async ({ thread }) => {
201
221
  await thread.post(Section({ children: "hi" }));
202
222
  });
@@ -211,7 +231,11 @@ describe("createChannel", () => {
211
231
  it("calls renderer.finish() once after a turn's run-loop resolves", async () => {
212
232
  const fake = new FakeAdapter();
213
233
  const agent = new FakeAgent();
214
- const channel = createChannel({ adapters: [fake], agent: () => agent });
234
+ const channel = createChannel({
235
+ identifyUser: "platform",
236
+ adapters: [fake],
237
+ agent: () => agent,
238
+ });
215
239
  channel.onMention(async ({ thread }) => {
216
240
  await thread.runAgent();
217
241
  });
@@ -225,7 +249,11 @@ describe("createChannel", () => {
225
249
  const fake = new FakeAdapter();
226
250
  const agent = new FakeAgent();
227
251
  const added = captureAddedMessages(agent);
228
- const channel = createChannel({ adapters: [fake], agent: () => agent });
252
+ const channel = createChannel({
253
+ identifyUser: "platform",
254
+ adapters: [fake],
255
+ agent: () => agent,
256
+ });
229
257
  channel.onMention(async ({ thread }) => {
230
258
  await thread.runAgent();
231
259
  });
@@ -248,7 +276,11 @@ describe("createChannel", () => {
248
276
  const agent = new FakeAgent();
249
277
  const added = captureAddedMessages(agent);
250
278
  const runs = trackRunAgentCalls(agent);
251
- const channel = createChannel({ adapters: [fake], agent: () => agent });
279
+ const channel = createChannel({
280
+ identifyUser: "platform",
281
+ adapters: [fake],
282
+ agent: () => agent,
283
+ });
252
284
  channel.onMention(async ({ thread }) => {
253
285
  await thread.runAgent();
254
286
  await thread.runAgent();
@@ -295,6 +327,7 @@ describe("createChannel", () => {
295
327
  ]);
296
328
  const sessionAgents = captureSessionAgents(fake);
297
329
  const channel = createChannel({
330
+ identifyUser: "platform",
298
331
  adapters: [fake],
299
332
  agent: () => agent,
300
333
  tools: [
@@ -342,7 +375,11 @@ describe("createChannel", () => {
342
375
  });
343
376
  return session;
344
377
  };
345
- const channel = createChannel({ adapters: [fake], agent: () => agent });
378
+ const channel = createChannel({
379
+ identifyUser: "platform",
380
+ adapters: [fake],
381
+ agent: () => agent,
382
+ });
346
383
  channel.onMention(async ({ thread }) => {
347
384
  await thread.runAgent();
348
385
  });
@@ -377,7 +414,11 @@ describe("createChannel", () => {
377
414
  });
378
415
  return session;
379
416
  };
380
- const channel = createChannel({ adapters: [fake], agent: () => agent });
417
+ const channel = createChannel({
418
+ identifyUser: "platform",
419
+ adapters: [fake],
420
+ agent: () => agent,
421
+ });
381
422
  channel.onMention(async ({ thread, message }) => {
382
423
  await thread.runAgent({ prompt: message.text });
383
424
  });
@@ -395,7 +436,11 @@ describe("createChannel", () => {
395
436
  const fake = new FakeAdapter();
396
437
  const agent = new FakeAgent();
397
438
  const added = captureAddedMessages(agent);
398
- const channel = createChannel({ adapters: [fake], agent: () => agent });
439
+ const channel = createChannel({
440
+ identifyUser: "platform",
441
+ adapters: [fake],
442
+ agent: () => agent,
443
+ });
399
444
  const parts = [
400
445
  { type: "text", text: "look" },
401
446
  {
@@ -424,7 +469,11 @@ describe("createChannel", () => {
424
469
  const fake = new FakeAdapter();
425
470
  const agent = new FakeAgent();
426
471
  const added = captureAddedMessages(agent);
427
- const channel = createChannel({ adapters: [fake], agent: () => agent });
472
+ const channel = createChannel({
473
+ identifyUser: "platform",
474
+ adapters: [fake],
475
+ agent: () => agent,
476
+ });
428
477
  const explicitParts = [
429
478
  { type: "text", text: "use this instead" },
430
479
  {
@@ -453,7 +502,11 @@ describe("createChannel", () => {
453
502
  it("dispatches a bound onClick handler on interaction", async () => {
454
503
  const fake = new FakeAdapter();
455
504
  const agent = new FakeAgent();
456
- const channel = createChannel({ adapters: [fake], agent: () => agent });
505
+ const channel = createChannel({
506
+ identifyUser: "platform",
507
+ adapters: [fake],
508
+ agent: () => agent,
509
+ });
457
510
  let clicked = false;
458
511
  channel.onMention(async ({ thread }) => {
459
512
  await thread.post(Actions({
@@ -481,7 +534,11 @@ describe("createChannel", () => {
481
534
  it("resolves a HITL awaitChoice with the element value when the event carries none (Telegram)", async () => {
482
535
  const fake = new FakeAdapter();
483
536
  const agent = new FakeAgent();
484
- const channel = createChannel({ adapters: [fake], agent: () => agent });
537
+ const channel = createChannel({
538
+ identifyUser: "platform",
539
+ adapters: [fake],
540
+ agent: () => agent,
541
+ });
485
542
  let chosen;
486
543
  channel.onMention(async ({ thread }) => {
487
544
  chosen = await thread.awaitChoice(Actions({
@@ -525,6 +582,7 @@ describe("createChannel", () => {
525
582
  };
526
583
  });
527
584
  const channel = createChannel({
585
+ identifyUser: "platform",
528
586
  adapters: [fake],
529
587
  agent: () => agent,
530
588
  context: [{ description: "channel-level", value: "always here" }],
@@ -546,7 +604,11 @@ describe("createChannel", () => {
546
604
  it("thread.postFile returns a capability-gated error when the adapter can't upload", async () => {
547
605
  const fake = new FakeAdapter();
548
606
  const agent = new FakeAgent();
549
- const channel = createChannel({ adapters: [fake], agent: () => agent });
607
+ const channel = createChannel({
608
+ identifyUser: "platform",
609
+ adapters: [fake],
610
+ agent: () => agent,
611
+ });
550
612
  let result;
551
613
  channel.onMention(async ({ thread }) => {
552
614
  result = await thread.postFile({
@@ -565,11 +627,20 @@ describe("createChannel", () => {
565
627
  it("thread.getMessages and thread.lookupUser surface the adapter's data", async () => {
566
628
  const fake = new FakeAdapter();
567
629
  fake.messages = [
568
- { user: { id: "u1", name: "Ada" }, text: "hi", ts: "1", isBot: false },
630
+ {
631
+ user: { id: "u1", kind: "human", name: "Ada" },
632
+ text: "hi",
633
+ ts: "1",
634
+ isBot: false,
635
+ },
569
636
  ];
570
- fake.user = { id: "u1", name: "Ada" };
637
+ fake.user = { id: "u1", kind: "human", name: "Ada" };
571
638
  const agent = new FakeAgent();
572
- const channel = createChannel({ adapters: [fake], agent: () => agent });
639
+ const channel = createChannel({
640
+ identifyUser: "platform",
641
+ adapters: [fake],
642
+ agent: () => agent,
643
+ });
573
644
  let history;
574
645
  let resolved;
575
646
  channel.onMention(async ({ thread }) => {
@@ -580,14 +651,23 @@ describe("createChannel", () => {
580
651
  fake.emitTurn({ userText: "hi", conversationKey: "c1" });
581
652
  await tick();
582
653
  expect(history).toEqual([
583
- { user: { id: "u1", name: "Ada" }, text: "hi", ts: "1", isBot: false },
654
+ {
655
+ user: { id: "u1", kind: "human", name: "Ada" },
656
+ text: "hi",
657
+ ts: "1",
658
+ isBot: false,
659
+ },
584
660
  ]);
585
- expect(resolved).toEqual({ id: "u1", name: "Ada" });
661
+ expect(resolved).toEqual({ id: "u1", kind: "human", name: "Ada" });
586
662
  });
587
663
  it("resolves awaitChoice when a matching interaction arrives", async () => {
588
664
  const fake = new FakeAdapter();
589
665
  const agent = new FakeAgent();
590
- const channel = createChannel({ adapters: [fake], agent: () => agent });
666
+ const channel = createChannel({
667
+ identifyUser: "platform",
668
+ adapters: [fake],
669
+ agent: () => agent,
670
+ });
591
671
  let choicePromise;
592
672
  channel.onMention(async ({ thread }) => {
593
673
  choicePromise = thread.awaitChoice(Actions({
@@ -622,6 +702,7 @@ describe("createChannel", () => {
622
702
  const fake = new FakeAdapter();
623
703
  const agent = new FakeAgent();
624
704
  const channel = createChannel({
705
+ identifyUser: "platform",
625
706
  adapters: [fake],
626
707
  agent: () => agent,
627
708
  store: { adapter: state, onLockConflict: "drop" },
@@ -654,6 +735,7 @@ describe("createChannel", () => {
654
735
  const fake = new FakeAdapter();
655
736
  const agent = new FakeAgent();
656
737
  const channel = createChannel({
738
+ identifyUser: "platform",
657
739
  adapters: [fake],
658
740
  agent: () => agent,
659
741
  store: { adapter: state, onLockConflict: "force" },
@@ -685,6 +767,7 @@ describe("createChannel", () => {
685
767
  const gate = new Promise((r) => (release = r));
686
768
  const fake = new FakeAdapter();
687
769
  const channel = createChannel({
770
+ identifyUser: "platform",
688
771
  adapters: [fake],
689
772
  agent: () => new FakeAgent(),
690
773
  store: { adapter: state },
@@ -716,6 +799,7 @@ describe("createChannel", () => {
716
799
  const gate1 = new Promise((r) => (release1 = r));
717
800
  const fake = new FakeAdapter();
718
801
  const channel = createChannel({
802
+ identifyUser: "platform",
719
803
  adapters: [fake],
720
804
  agent: () => new FakeAgent(),
721
805
  store: { adapter: state, concurrency: "serial" },
@@ -750,6 +834,7 @@ describe("createChannel", () => {
750
834
  const gate = new Promise((r) => (release = r));
751
835
  const fake = new FakeAdapter();
752
836
  const channel = createChannel({
837
+ identifyUser: "platform",
753
838
  adapters: [fake],
754
839
  agent: () => new FakeAgent(),
755
840
  store: { adapter: state, concurrency: "drop" },
@@ -778,6 +863,7 @@ describe("createChannel", () => {
778
863
  const fake = new FakeAdapter();
779
864
  const { agents: seen } = captureSessionAgents(fake);
780
865
  const channel = createChannel({
866
+ identifyUser: "platform",
781
867
  adapters: [fake],
782
868
  agent: prototype, // singleton, not factory
783
869
  store: { adapter: state },
@@ -814,6 +900,7 @@ describe("createChannel", () => {
814
900
  bad.clone = () => bad;
815
901
  const fake = new FakeAdapter();
816
902
  const channel = createChannel({
903
+ identifyUser: "platform",
817
904
  adapters: [fake],
818
905
  agent: bad,
819
906
  store: { adapter: state },
@@ -840,6 +927,7 @@ describe("createChannel", () => {
840
927
  // every call. Turn concurrency is parallel by default, so without isolation
841
928
  // both turns would run on `shared` and append into its one `messages` array.
842
929
  const channel = createChannel({
930
+ identifyUser: "platform",
843
931
  adapters: [fake],
844
932
  agent: (threadId) => {
845
933
  shared.threadId = threadId;
@@ -908,6 +996,7 @@ describe("createChannel", () => {
908
996
  const agent = new StatefulAgent();
909
997
  const fake = new FakeAdapter();
910
998
  const channel = createChannel({
999
+ identifyUser: "platform",
911
1000
  adapters: [fake],
912
1001
  agent: () => agent,
913
1002
  store: { adapter: state },
@@ -934,6 +1023,7 @@ describe("createChannel", () => {
934
1023
  agent.runAgent = async (parameters, subscriber) => FakeAgent.prototype.runAgent.call(agent, parameters, subscriber);
935
1024
  const fake = new FakeAdapter();
936
1025
  const channel = createChannel({
1026
+ identifyUser: "platform",
937
1027
  adapters: [fake],
938
1028
  agent: () => agent,
939
1029
  store: { adapter: state },
@@ -956,6 +1046,7 @@ describe("createChannel", () => {
956
1046
  const fake = new FakeAdapter();
957
1047
  const agent = new FakeAgent();
958
1048
  const channel = createChannel({
1049
+ identifyUser: "platform",
959
1050
  adapters: [fake],
960
1051
  agent: () => agent,
961
1052
  store: { adapter: state },
@@ -980,36 +1071,32 @@ describe("createChannel", () => {
980
1071
  await sink.onTurn({ ...base, eventId: "E2" });
981
1072
  expect(runs).toBe(2);
982
1073
  });
983
- it("throws when identity is set without transcripts", () => {
1074
+ it("configures transcripts without a second identity callback", async () => {
984
1075
  const fake = new FakeAdapter();
985
- expect(() => createChannel({
986
- adapters: [fake],
987
- store: { identity: () => "key" },
988
- })).toThrow("createChannel: `identity` and `transcripts` must be configured together.");
989
- });
990
- it("throws when transcripts is set without identity", () => {
991
- const fake = new FakeAdapter();
992
- expect(() => createChannel({
1076
+ const channel = createChannel({
993
1077
  adapters: [fake],
1078
+ identifyUser: "platform",
994
1079
  store: { transcripts: { maxPerUser: 100 } },
995
- })).toThrow("createChannel: `identity` and `transcripts` must be configured together.");
1080
+ });
1081
+ await channel.ɵruntime.start();
1082
+ expect(channel.transcripts).toBeDefined();
996
1083
  });
997
- it("stamps message.userKey when identity resolves a key", async () => {
1084
+ it("stamps message.user when identifyUser resolves an application user", async () => {
998
1085
  const state = new MemoryStore();
999
1086
  const fake = new FakeAdapter();
1000
1087
  const agent = new FakeAgent();
1001
1088
  const channel = createChannel({
1002
1089
  adapters: [fake],
1003
1090
  agent: () => agent,
1091
+ identifyUser: () => ({ id: "user@example.com", name: "Alice" }),
1004
1092
  store: {
1005
1093
  adapter: state,
1006
- identity: () => "user@example.com",
1007
1094
  transcripts: {},
1008
1095
  },
1009
1096
  });
1010
1097
  let capturedKey;
1011
1098
  channel.onMention(async ({ message }) => {
1012
- capturedKey = message.userKey;
1099
+ capturedKey = message.user?.id;
1013
1100
  });
1014
1101
  await channel.ɵruntime.start();
1015
1102
  const sink = fake.getSink();
@@ -1018,7 +1105,7 @@ describe("createChannel", () => {
1018
1105
  replyTarget: {},
1019
1106
  userText: "hello",
1020
1107
  platform: "fake",
1021
- user: { id: "u1" },
1108
+ actor: { id: "u1", kind: "human" },
1022
1109
  });
1023
1110
  expect(capturedKey).toBe("user@example.com");
1024
1111
  });
@@ -1027,11 +1114,11 @@ describe("createChannel", () => {
1027
1114
  const fake = new FakeAdapter();
1028
1115
  const agent = new FakeAgent();
1029
1116
  const channel = createChannel({
1117
+ identifyUser: "platform",
1030
1118
  adapters: [fake],
1031
1119
  agent: () => agent,
1032
1120
  store: {
1033
1121
  adapter: state,
1034
- identity: () => "alice@example.com",
1035
1122
  transcripts: { maxPerUser: 50 },
1036
1123
  },
1037
1124
  });
@@ -1041,11 +1128,11 @@ describe("createChannel", () => {
1041
1128
  const thread = { platform: "fake", conversationKey: "c1" };
1042
1129
  // Directly append via channel.transcripts
1043
1130
  await channel.transcripts.append(thread, { role: "user", text: "hi there" }, {
1044
- userKey: "alice@example.com",
1131
+ userId: "alice@example.com",
1045
1132
  });
1046
- await channel.transcripts.append(thread, { role: "assistant", text: "hello!" }, { userKey: "alice@example.com" });
1133
+ await channel.transcripts.append(thread, { role: "assistant", text: "hello!" }, { userId: "alice@example.com" });
1047
1134
  const entries = await channel.transcripts.list({
1048
- userKey: "alice@example.com",
1135
+ userId: "alice@example.com",
1049
1136
  });
1050
1137
  expect(entries).toHaveLength(2);
1051
1138
  expect(entries[0].role).toBe("user");
@@ -1060,9 +1147,9 @@ describe("createChannel", () => {
1060
1147
  const channel = createChannel({
1061
1148
  adapters: [fake],
1062
1149
  agent: () => agent,
1150
+ identifyUser: () => ({ id: "u@x.com", name: "User" }),
1063
1151
  store: {
1064
1152
  adapter: state,
1065
- identity: () => "u@x.com",
1066
1153
  transcripts: {},
1067
1154
  },
1068
1155
  });
@@ -1094,18 +1181,18 @@ describe("createChannel", () => {
1094
1181
  // Seed one prior cross-platform entry (different platform label) so we can
1095
1182
  // assert it shows up in the injected context. Seeded post-start: transcripts
1096
1183
  // are only available once the backend is resolved in start().
1097
- await channel.transcripts.append({ platform: "discord", conversationKey: "other" }, { role: "user", text: "remembered from discord" }, { userKey: "u@x.com" });
1184
+ await channel.transcripts.append({ platform: "discord", conversationKey: "other" }, { role: "user", text: "remembered from discord" }, { userId: "u@x.com" });
1098
1185
  const sink = fake.getSink();
1099
1186
  await sink.onTurn({
1100
1187
  conversationKey: "c1",
1101
1188
  replyTarget: {},
1102
1189
  userText: "hello from fake",
1103
1190
  platform: "fake",
1104
- user: { id: "u1" },
1191
+ actor: { id: "u1", kind: "human" },
1105
1192
  });
1106
1193
  // Append side: both the user turn and the assistant reply are recorded,
1107
1194
  // oldest-first (after the seeded discord entry).
1108
- const entries = await channel.transcripts.list({ userKey: "u@x.com" });
1195
+ const entries = await channel.transcripts.list({ userId: "u@x.com" });
1109
1196
  const fakeEntries = entries.filter((e) => e.platform === "fake");
1110
1197
  expect(fakeEntries).toHaveLength(2);
1111
1198
  expect(fakeEntries[0].role).toBe("user");
@@ -1123,6 +1210,7 @@ describe("createChannel", () => {
1123
1210
  const fake = new FakeAdapter();
1124
1211
  const agent = new FakeAgent();
1125
1212
  const channel = createChannel({
1213
+ identifyUser: "platform",
1126
1214
  adapters: [fake],
1127
1215
  agent: () => agent,
1128
1216
  store: {
@@ -1159,6 +1247,7 @@ describe("createChannel lock and dedup edge cases", () => {
1159
1247
  const fake = new FakeAdapter();
1160
1248
  // Use a separate channel so the throwing handler is isolated.
1161
1249
  const bot1 = createChannel({
1250
+ identifyUser: "platform",
1162
1251
  adapters: [fake],
1163
1252
  store: { adapter: state, onLockConflict: "drop", lockTtl: 5000 },
1164
1253
  });
@@ -1196,6 +1285,7 @@ describe("createChannel lock and dedup edge cases", () => {
1196
1285
  let callbackMessageText;
1197
1286
  const fake = new FakeAdapter();
1198
1287
  const channel = createChannel({
1288
+ identifyUser: "platform",
1199
1289
  adapters: [fake],
1200
1290
  store: {
1201
1291
  adapter: state,
@@ -1233,6 +1323,7 @@ describe("createChannel lock and dedup edge cases", () => {
1233
1323
  const gate = new Promise((r) => (release = r));
1234
1324
  const fake = new FakeAdapter();
1235
1325
  const channel = createChannel({
1326
+ identifyUser: "platform",
1236
1327
  adapters: [fake],
1237
1328
  store: {
1238
1329
  adapter: state,
@@ -1257,43 +1348,48 @@ describe("createChannel lock and dedup edge cases", () => {
1257
1348
  await Promise.all([p1, p2]);
1258
1349
  expect(runs).toBe(2);
1259
1350
  });
1260
- it("identity throws: handler still runs and userKey is undefined", async () => {
1351
+ it("identifyUser errors fail the trigger before its handler runs", async () => {
1261
1352
  const state = new MemoryStore();
1262
1353
  const fake = new FakeAdapter();
1263
1354
  const channel = createChannel({
1264
1355
  adapters: [fake],
1356
+ identifyUser: () => {
1357
+ throw new Error("identity failed");
1358
+ },
1265
1359
  store: {
1266
1360
  adapter: state,
1267
- identity: () => {
1268
- throw new Error("x");
1269
- },
1270
1361
  transcripts: {},
1271
1362
  },
1272
1363
  });
1273
- let capturedUserKey = "SENTINEL";
1274
- channel.onMention(async ({ message }) => {
1275
- capturedUserKey = message.userKey;
1364
+ let handled = false;
1365
+ channel.onMention(async () => {
1366
+ handled = true;
1276
1367
  });
1277
1368
  await channel.ɵruntime.start();
1278
1369
  const sink = fake.getSink();
1279
- await sink.onTurn({
1370
+ const delivery = sink.onTurn({
1280
1371
  conversationKey: "c1",
1281
1372
  replyTarget: {},
1282
1373
  userText: "hi",
1283
1374
  platform: "fake",
1284
1375
  });
1285
- expect(capturedUserKey).toBeUndefined();
1376
+ await expect(delivery).rejects.toMatchObject({
1377
+ code: "channel_identity_failed",
1378
+ cause: expect.objectContaining({ message: "identity failed" }),
1379
+ });
1380
+ expect(handled).toBe(false);
1286
1381
  });
1287
- it("identity returns null: userKey is undefined", async () => {
1382
+ it("identifyUser may return null without substituting the provider actor", async () => {
1288
1383
  const state = new MemoryStore();
1289
1384
  const fake = new FakeAdapter();
1290
1385
  const channel = createChannel({
1291
1386
  adapters: [fake],
1292
- store: { adapter: state, identity: () => null, transcripts: {} },
1387
+ identifyUser: () => null,
1388
+ store: { adapter: state, transcripts: {} },
1293
1389
  });
1294
- let capturedUserKey = "SENTINEL";
1390
+ let capturedUser = "SENTINEL";
1295
1391
  channel.onMention(async ({ message }) => {
1296
- capturedUserKey = message.userKey;
1392
+ capturedUser = message.user;
1297
1393
  });
1298
1394
  await channel.ɵruntime.start();
1299
1395
  const sink = fake.getSink();
@@ -1303,13 +1399,14 @@ describe("createChannel lock and dedup edge cases", () => {
1303
1399
  userText: "hi",
1304
1400
  platform: "fake",
1305
1401
  });
1306
- expect(capturedUserKey).toBeUndefined();
1402
+ expect(capturedUser).toBeNull();
1307
1403
  });
1308
1404
  it("dedup+lock ordering: deduped turn never takes the lock", async () => {
1309
1405
  const state = new MemoryStore();
1310
1406
  let runs = 0;
1311
1407
  const fake = new FakeAdapter();
1312
1408
  const channel = createChannel({
1409
+ identifyUser: "platform",
1313
1410
  adapters: [fake],
1314
1411
  store: { adapter: state, onLockConflict: "drop" },
1315
1412
  });
@@ -1349,6 +1446,7 @@ describe("createChannel lock and dedup edge cases", () => {
1349
1446
  let runs = 0;
1350
1447
  const fake = new FakeAdapter();
1351
1448
  const channel = createChannel({
1449
+ identifyUser: "platform",
1352
1450
  adapters: [fake],
1353
1451
  store: { adapter: state },
1354
1452
  });
@@ -1374,6 +1472,7 @@ describe("createChannel lock and dedup edge cases", () => {
1374
1472
  const gate = new Promise((r) => (releaseGate = r));
1375
1473
  const fake = new FakeAdapter();
1376
1474
  const channel = createChannel({
1475
+ identifyUser: "platform",
1377
1476
  adapters: [fake],
1378
1477
  store: { adapter: state, onLockConflict: "drop" },
1379
1478
  });
@@ -1416,6 +1515,7 @@ describe("createChannel lock and dedup edge cases", () => {
1416
1515
  let runs = 0;
1417
1516
  const fake = new FakeAdapter();
1418
1517
  const channel = createChannel({
1518
+ identifyUser: "platform",
1419
1519
  adapters: [fake],
1420
1520
  store: { adapter: state },
1421
1521
  });
@@ -1442,7 +1542,10 @@ describe("createChannel lock and dedup edge cases", () => {
1442
1542
  describe("createChannel slash commands", () => {
1443
1543
  it("routes a command to its handler with the raw text", async () => {
1444
1544
  const fake = new FakeAdapter();
1445
- const channel = createChannel({ adapters: [fake] });
1545
+ const channel = createChannel({
1546
+ identifyUser: "platform",
1547
+ adapters: [fake],
1548
+ });
1446
1549
  let seen;
1447
1550
  channel.onCommand("triage", ({ command, text }) => {
1448
1551
  seen = { command, text };
@@ -1453,7 +1556,10 @@ describe("createChannel slash commands", () => {
1453
1556
  });
1454
1557
  it("ignores a command with no registered handler", async () => {
1455
1558
  const fake = new FakeAdapter();
1456
- const channel = createChannel({ adapters: [fake] });
1559
+ const channel = createChannel({
1560
+ identifyUser: "platform",
1561
+ adapters: [fake],
1562
+ });
1457
1563
  let fired = false;
1458
1564
  channel.onCommand("triage", () => {
1459
1565
  fired = true;
@@ -1466,6 +1572,7 @@ describe("createChannel slash commands", () => {
1466
1572
  let captured;
1467
1573
  const fake = new FakeAdapter();
1468
1574
  const channel = createChannel({
1575
+ identifyUser: "platform",
1469
1576
  adapters: [fake],
1470
1577
  commands: [
1471
1578
  defineChannelCommand({
@@ -1487,7 +1594,10 @@ describe("createChannel slash commands", () => {
1487
1594
  });
1488
1595
  it("hands declared commands to adapters that implement registerCommands", async () => {
1489
1596
  const fake = new FakeAdapter();
1490
- const channel = createChannel({ adapters: [fake] });
1597
+ const channel = createChannel({
1598
+ identifyUser: "platform",
1599
+ adapters: [fake],
1600
+ });
1491
1601
  channel.onCommand("triage", () => { });
1492
1602
  channel.onCommand("status", () => { });
1493
1603
  await channel.ɵruntime.start();
@@ -1500,16 +1610,124 @@ describe("createChannel slash commands", () => {
1500
1610
  const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
1501
1611
  try {
1502
1612
  const bad = new FakeAdapter({ platform: "telegram", failStart: true });
1613
+ const badStop = vi.spyOn(bad, "stop");
1503
1614
  const good = new FakeAdapter({ platform: "slack" });
1504
- const channel = createChannel({ adapters: [bad, good] });
1615
+ const channel = createChannel({
1616
+ identifyUser: "platform",
1617
+ adapters: [bad, good],
1618
+ });
1505
1619
  await expect(channel.ɵruntime.start()).resolves.toBeUndefined();
1506
1620
  expect(good.started).toBe(true);
1621
+ expect(badStop).toHaveBeenCalledTimes(1);
1507
1622
  expect(errSpy.mock.calls.some((c) => String(c[0]).includes("telegram"))).toBe(true);
1508
1623
  }
1509
1624
  finally {
1510
1625
  errSpy.mockRestore();
1511
1626
  }
1512
1627
  });
1628
+ it("does not register commands on an adapter whose start failed", async () => {
1629
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
1630
+ try {
1631
+ const failed = new FakeAdapter({ platform: "telegram", failStart: true });
1632
+ const failedRegister = vi.spyOn(failed, "registerCommands");
1633
+ const healthy = new FakeAdapter({ platform: "slack" });
1634
+ const channel = createChannel({
1635
+ identifyUser: "platform",
1636
+ adapters: [failed, healthy],
1637
+ });
1638
+ channel.onCommand("triage", () => { });
1639
+ await expect(channel.ɵruntime.start()).resolves.toBeUndefined();
1640
+ expect(failedRegister).not.toHaveBeenCalled();
1641
+ expect(healthy.registeredCommands?.map(({ name }) => name)).toEqual([
1642
+ "triage",
1643
+ ]);
1644
+ }
1645
+ finally {
1646
+ errSpy.mockRestore();
1647
+ }
1648
+ });
1649
+ it("start() rejects and rolls back direct adapters when the managed adapter fails", async () => {
1650
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
1651
+ try {
1652
+ const direct = new FakeAdapter({ platform: "slack" });
1653
+ const directStop = vi.spyOn(direct, "stop");
1654
+ const managed = Object.assign(new FakeAdapter({ platform: "intelligence", failStart: true }), { __intelligenceChannel: true });
1655
+ const managedStop = vi.spyOn(managed, "stop");
1656
+ const channel = createChannel({
1657
+ identifyUser: "platform",
1658
+ adapters: [direct, managed],
1659
+ });
1660
+ await expect(channel.ɵruntime.start()).rejects.toThrow("failed to start its managed Intelligence adapter");
1661
+ expect(direct.started).toBe(true);
1662
+ expect(directStop).toHaveBeenCalledTimes(1);
1663
+ expect(managedStop).toHaveBeenCalledTimes(1);
1664
+ }
1665
+ finally {
1666
+ errSpy.mockRestore();
1667
+ }
1668
+ });
1669
+ it("managed-start rollback keeps stopping adapters after a synchronous stop throw", async () => {
1670
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
1671
+ try {
1672
+ const throwing = new FakeAdapter({ platform: "slack" });
1673
+ throwing.stop = vi.fn(() => {
1674
+ throw new Error("synchronous stop failure");
1675
+ });
1676
+ const healthy = new FakeAdapter({ platform: "teams" });
1677
+ const healthyStop = vi.spyOn(healthy, "stop");
1678
+ const managed = Object.assign(new FakeAdapter({ platform: "intelligence", failStart: true }), { __intelligenceChannel: true });
1679
+ const managedStop = vi.spyOn(managed, "stop");
1680
+ const channel = createChannel({
1681
+ identifyUser: "platform",
1682
+ adapters: [throwing, healthy, managed],
1683
+ });
1684
+ await expect(channel.ɵruntime.start()).rejects.toThrow("failed to start its managed Intelligence adapter");
1685
+ expect(throwing.stop).toHaveBeenCalledTimes(1);
1686
+ expect(healthyStop).toHaveBeenCalledTimes(1);
1687
+ expect(managedStop).toHaveBeenCalledTimes(1);
1688
+ }
1689
+ finally {
1690
+ errSpy.mockRestore();
1691
+ }
1692
+ });
1693
+ it("managed-start rollback times out a stop that never settles", async () => {
1694
+ vi.useFakeTimers();
1695
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
1696
+ try {
1697
+ const stuck = new FakeAdapter({ platform: "slack" });
1698
+ stuck.stop = vi.fn(() => new Promise(() => undefined));
1699
+ const managed = Object.assign(new FakeAdapter({ platform: "intelligence", failStart: true }), { __intelligenceChannel: true });
1700
+ const channel = createChannel({
1701
+ identifyUser: "platform",
1702
+ adapters: [stuck, managed],
1703
+ });
1704
+ const starting = expect(channel.ɵruntime.start()).rejects.toThrow("failed to start its managed Intelligence adapter");
1705
+ await vi.advanceTimersByTimeAsync(5_000);
1706
+ await starting;
1707
+ expect(stuck.stop).toHaveBeenCalledOnce();
1708
+ expect(errSpy.mock.calls.some((call) => call.some((part) => String(part).includes("stop timed out")))).toBe(true);
1709
+ }
1710
+ finally {
1711
+ errSpy.mockRestore();
1712
+ vi.useRealTimers();
1713
+ }
1714
+ });
1715
+ it("start() stops a rejecting adapter when every adapter fails", async () => {
1716
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
1717
+ try {
1718
+ const adapter = new FakeAdapter({ platform: "slack", failStart: true });
1719
+ const stop = vi.spyOn(adapter, "stop");
1720
+ const channel = createChannel({
1721
+ identifyUser: "platform",
1722
+ adapters: [adapter],
1723
+ });
1724
+ await expect(channel.ɵruntime.start()).rejects.toThrow("all 1 adapter(s) failed to connect");
1725
+ expect(stop).toHaveBeenCalledTimes(1);
1726
+ }
1727
+ finally {
1728
+ errSpy.mockRestore();
1729
+ }
1730
+ });
1513
1731
  it("start() resolves and the healthy adapter still receives commands when another adapter's registerCommands() rejects", async () => {
1514
1732
  const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
1515
1733
  try {
@@ -1518,7 +1736,10 @@ describe("createChannel slash commands", () => {
1518
1736
  failRegisterCommands: true,
1519
1737
  });
1520
1738
  const good = new FakeAdapter({ platform: "slack" });
1521
- const channel = createChannel({ adapters: [bad, good] });
1739
+ const channel = createChannel({
1740
+ identifyUser: "platform",
1741
+ adapters: [bad, good],
1742
+ });
1522
1743
  channel.onCommand("triage", () => { });
1523
1744
  await expect(channel.ɵruntime.start()).resolves.toBeUndefined();
1524
1745
  expect(good.started).toBe(true);
@@ -1535,7 +1756,10 @@ describe("createChannel slash commands", () => {
1535
1756
  const bad = new FakeAdapter({ platform: "telegram", failStop: true });
1536
1757
  const good = new FakeAdapter({ platform: "slack" });
1537
1758
  const stopSpy = vi.spyOn(good, "stop");
1538
- const channel = createChannel({ adapters: [bad, good] });
1759
+ const channel = createChannel({
1760
+ identifyUser: "platform",
1761
+ adapters: [bad, good],
1762
+ });
1539
1763
  await channel.ɵruntime.start();
1540
1764
  await expect(channel.ɵruntime.stop()).resolves.toBeUndefined();
1541
1765
  expect(stopSpy).toHaveBeenCalled();
@@ -1547,7 +1771,10 @@ describe("createChannel slash commands", () => {
1547
1771
  });
1548
1772
  it("exposes attached adapters through a read-only, non-mutable-through accessor", () => {
1549
1773
  const fake = new FakeAdapter();
1550
- const channel = createChannel({ adapters: [fake] });
1774
+ const channel = createChannel({
1775
+ identifyUser: "platform",
1776
+ adapters: [fake],
1777
+ });
1551
1778
  expect(channel.adapters).toContain(fake);
1552
1779
  expect(channel.adapters).toHaveLength(1);
1553
1780
  // The returned snapshot is a copy: mutating it must not affect the channel.
@@ -1555,7 +1782,7 @@ describe("createChannel slash commands", () => {
1555
1782
  expect(channel.adapters).toHaveLength(1);
1556
1783
  });
1557
1784
  it("reflects an adapter attached via addAdapter in the adapters accessor", () => {
1558
- const channel = createChannel({});
1785
+ const channel = createChannel({ identifyUser: "platform" });
1559
1786
  expect(channel.adapters).toHaveLength(0);
1560
1787
  const fake = new FakeAdapter();
1561
1788
  channel.ɵruntime.addAdapter(fake);