@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.
- package/README.md +35 -14
- package/dist/action-registry.d.ts +15 -3
- package/dist/action-registry.d.ts.map +1 -1
- package/dist/action-registry.js +61 -9
- package/dist/action-registry.test.js +107 -2
- package/dist/action-store.d.ts +23 -0
- package/dist/action-store.d.ts.map +1 -1
- package/dist/action-store.js +11 -0
- package/dist/canonical-run-loop.test.js +6 -1
- package/dist/channel-identity.test.d.ts +2 -0
- package/dist/channel-identity.test.d.ts.map +1 -0
- package/dist/channel-identity.test.js +297 -0
- package/dist/channel-memory.test.d.ts +2 -0
- package/dist/channel-memory.test.d.ts.map +1 -0
- package/dist/channel-memory.test.js +199 -0
- package/dist/commands.d.ts +5 -3
- package/dist/commands.d.ts.map +1 -1
- package/dist/create-channel.d.ts +30 -56
- package/dist/create-channel.d.ts.map +1 -1
- package/dist/create-channel.foundations.test.js +30 -7
- package/dist/create-channel.js +209 -72
- package/dist/create-channel.test.js +293 -66
- package/dist/hitl-continuation.test.d.ts +2 -0
- package/dist/hitl-continuation.test.d.ts.map +1 -0
- package/dist/hitl-continuation.test.js +393 -0
- package/dist/identity.d.ts +51 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +56 -0
- package/dist/identity.test.d.ts +2 -0
- package/dist/identity.test.d.ts.map +1 -0
- package/dist/identity.test.js +109 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/managed-v1-await-choice-guard.test.js +4 -1
- package/dist/memory.d.ts +23 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +25 -0
- package/dist/modal-submit.test.js +21 -6
- package/dist/open-modal.test.js +16 -4
- package/dist/platform-adapter.d.ts +29 -6
- package/dist/platform-adapter.d.ts.map +1 -1
- package/dist/platform-adapter.test.js +2 -0
- package/dist/reactions.test.js +49 -11
- package/dist/run-loop.test.js +30 -5
- package/dist/sanitize-agent-events.test.js +2 -0
- package/dist/source-platform.test.js +24 -9
- package/dist/state/kv-action-store.d.ts.map +1 -1
- package/dist/state/kv-action-store.js +1 -0
- package/dist/state/kv-action-store.test.js +11 -0
- package/dist/state/memory-store.d.ts +1 -0
- package/dist/state/memory-store.d.ts.map +1 -1
- package/dist/state/memory-store.js +9 -0
- package/dist/state/state-store.d.ts +2 -0
- package/dist/state/state-store.d.ts.map +1 -1
- package/dist/telemetry/create-channel-telemetry.test.js +4 -2
- package/dist/telemetry/e2e-telemetry.test.js +1 -0
- package/dist/telemetry/install-id.test.js +5 -0
- package/dist/testing/fake-adapter.d.ts +26 -9
- package/dist/testing/fake-adapter.d.ts.map +1 -1
- package/dist/testing/fake-adapter.js +65 -21
- package/dist/testing/state-store-conformance.d.ts.map +1 -1
- package/dist/testing/state-store-conformance.js +16 -0
- package/dist/thread-capabilities.test.js +29 -9
- package/dist/thread-ephemeral.test.js +8 -5
- package/dist/thread-promise-contract.test.js +4 -0
- package/dist/thread-reactions.test.js +8 -2
- package/dist/thread.d.ts +50 -7
- package/dist/thread.d.ts.map +1 -1
- package/dist/thread.js +163 -17
- package/dist/thread.test.js +4 -0
- package/dist/tools.d.ts +5 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +2 -1
- package/dist/transcripts.d.ts +6 -13
- package/dist/transcripts.d.ts.map +1 -1
- package/dist/transcripts.js +12 -12
- package/dist/transcripts.test.js +28 -29
- package/dist/welcome.test.d.ts +2 -0
- package/dist/welcome.test.d.ts.map +1 -0
- package/dist/welcome.test.js +53 -0
- package/package.json +5 -5
package/dist/create-channel.js
CHANGED
|
@@ -7,10 +7,12 @@ import { Thread } from "./thread.js";
|
|
|
7
7
|
import { sanitizeAgentEventStream } from "./sanitize-agent-events.js";
|
|
8
8
|
import { normalizeEmoji, toCanonicalEmoji, renderToIR, } from "@copilotkit/channels-ui";
|
|
9
9
|
import { Transcripts } from "./transcripts.js";
|
|
10
|
+
import { resolveChannelUser } from "./identity.js";
|
|
10
11
|
import { ChannelTelemetry } from "./telemetry/channel-telemetry.js";
|
|
11
12
|
import { errorClass, normalizePlatform } from "./telemetry/sanitize-error.js";
|
|
12
13
|
import { createRequire } from "node:module";
|
|
13
14
|
const pkg = createRequire(import.meta.url)("../package.json");
|
|
15
|
+
const ADAPTER_ROLLBACK_TIMEOUT_MS = 5_000;
|
|
14
16
|
function storeKind(s) {
|
|
15
17
|
const n = s.constructor?.name;
|
|
16
18
|
if (n === "MemoryStore")
|
|
@@ -138,8 +140,9 @@ function msgFromTurn(turn) {
|
|
|
138
140
|
return {
|
|
139
141
|
text: turn.userText,
|
|
140
142
|
contentParts: turn.contentParts,
|
|
141
|
-
user:
|
|
142
|
-
|
|
143
|
+
user: null,
|
|
144
|
+
actor: turn.actor,
|
|
145
|
+
ref: turn.messageRef ?? { id: "" },
|
|
143
146
|
platform: turn.platform,
|
|
144
147
|
operation: turn.operation,
|
|
145
148
|
eventId: turn.eventId,
|
|
@@ -147,21 +150,42 @@ function msgFromTurn(turn) {
|
|
|
147
150
|
deliveryId: turn.deliveryId,
|
|
148
151
|
};
|
|
149
152
|
}
|
|
153
|
+
function identityContextFromIngress(provider, event, trigger) {
|
|
154
|
+
const actor = Object.freeze({
|
|
155
|
+
...(event.actor ?? { id: "", kind: "unknown" }),
|
|
156
|
+
});
|
|
157
|
+
const tenant = Object.freeze({
|
|
158
|
+
...(event.identityContext?.tenant ?? { id: "" }),
|
|
159
|
+
});
|
|
160
|
+
const installation = Object.freeze({
|
|
161
|
+
...(event.identityContext?.installation ?? { id: "" }),
|
|
162
|
+
});
|
|
163
|
+
const conversation = Object.freeze({
|
|
164
|
+
...(event.identityContext?.conversation ?? {
|
|
165
|
+
id: event.conversationKey ?? "",
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
const normalizedEvent = Object.freeze({
|
|
169
|
+
...event.identityContext?.event,
|
|
170
|
+
});
|
|
171
|
+
return Object.freeze({
|
|
172
|
+
provider,
|
|
173
|
+
tenant,
|
|
174
|
+
installation,
|
|
175
|
+
actor,
|
|
176
|
+
conversation,
|
|
177
|
+
trigger: event.identityContext?.trigger ?? trigger,
|
|
178
|
+
event: normalizedEvent,
|
|
179
|
+
raw: event.identityContext?.raw,
|
|
180
|
+
...(event.identityContext?.lookupProfile
|
|
181
|
+
? { lookupProfile: event.identityContext.lookupProfile }
|
|
182
|
+
: {}),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
150
185
|
/** Resolve the provider that produced an event without changing adapter identity. */
|
|
151
186
|
function ingressPlatform(adapter, event) {
|
|
152
187
|
return event.platform ?? adapter.platform;
|
|
153
188
|
}
|
|
154
|
-
/**
|
|
155
|
-
* Enforce managed Intelligence Channel exclusivity. Managed and direct
|
|
156
|
-
* delivery are alternative modes on a Channel: Intelligence holds provider
|
|
157
|
-
* credentials, or the runtime does, never both.
|
|
158
|
-
*/
|
|
159
|
-
function assertExclusive(adapters) {
|
|
160
|
-
if (adapters.some((a) => a.__intelligenceChannel) && adapters.length > 1) {
|
|
161
|
-
throw new Error("The managed Intelligence adapter must be the only adapter on a Channel — " +
|
|
162
|
-
"managed and direct delivery are alternative modes.");
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
189
|
/**
|
|
166
190
|
* Resolve the persistence backend at start(): an explicit `store.adapter` wins
|
|
167
191
|
* (silently); otherwise an adapter-provided `stateStore` is used (warning when
|
|
@@ -179,16 +203,15 @@ function resolveBackend(explicit, adapters) {
|
|
|
179
203
|
return providers[0]?.stateStore ?? new MemoryStore();
|
|
180
204
|
}
|
|
181
205
|
export function createChannel(opts) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
(
|
|
185
|
-
throw new Error("createChannel: `identity` and `transcripts` must be configured together.");
|
|
206
|
+
if (opts.identifyUser !== "platform" &&
|
|
207
|
+
typeof opts.identifyUser !== "function") {
|
|
208
|
+
throw new Error('createChannel: `identifyUser` must be "platform" or a callback');
|
|
186
209
|
}
|
|
210
|
+
const cfg = opts.store ?? {};
|
|
187
211
|
// Adapters can be supplied up front or added later via
|
|
188
212
|
// `channel.ɵruntime.addAdapter` (before `channel.ɵruntime.start()`). The
|
|
189
213
|
// runtime uses the latter to attach Channel delivery.
|
|
190
214
|
const adapters = [...(opts.adapters ?? [])];
|
|
191
|
-
assertExclusive(adapters);
|
|
192
215
|
let started = false;
|
|
193
216
|
// Backend, transcripts, telemetry, the action registry, and component
|
|
194
217
|
// registration are resolved in `ɵruntime.start()` — not at construction —
|
|
@@ -238,6 +261,7 @@ export function createChannel(opts) {
|
|
|
238
261
|
const context = opts.context ?? [];
|
|
239
262
|
const mentionHandlers = [];
|
|
240
263
|
const messageHandlers = [];
|
|
264
|
+
const welcomeHandlers = [];
|
|
241
265
|
const threadStartedHandlers = [];
|
|
242
266
|
const interactionHandlers = new Map();
|
|
243
267
|
const interruptHandlers = new Map();
|
|
@@ -248,6 +272,7 @@ export function createChannel(opts) {
|
|
|
248
272
|
const modalSubmitHandlers = new Map();
|
|
249
273
|
const modalCloseHandlers = new Map();
|
|
250
274
|
const waiters = new Map();
|
|
275
|
+
let intelligenceMemoryAvailable = false;
|
|
251
276
|
// Recomputed on start() so tools added via channel.tool() before start are picked up.
|
|
252
277
|
let toolDescriptors = toAgentToolDescriptors([...toolMap.values()]);
|
|
253
278
|
function makeThread(adapter, replyTarget, conversationKey, extras) {
|
|
@@ -269,8 +294,13 @@ export function createChannel(opts) {
|
|
|
269
294
|
state: backend,
|
|
270
295
|
stateSchema: cfg.state,
|
|
271
296
|
transcripts,
|
|
272
|
-
userKey: extras?.userKey,
|
|
273
297
|
message: extras?.message,
|
|
298
|
+
user: extras?.user ?? null,
|
|
299
|
+
actor: extras?.actor ?? { id: "unknown", kind: "unknown" },
|
|
300
|
+
channelName: opts.name ?? adapter.platform,
|
|
301
|
+
threadId: adapter.getCanonicalThreadId?.(replyTarget) ?? conversationKey,
|
|
302
|
+
interactionActionId: extras?.interactionActionId,
|
|
303
|
+
intelligenceMemoryAvailable,
|
|
274
304
|
telemetry,
|
|
275
305
|
};
|
|
276
306
|
return new Thread(deps);
|
|
@@ -312,26 +342,19 @@ export function createChannel(opts) {
|
|
|
312
342
|
console.warn(`[channel] dedup check failed for ${platform}; processing without dedup`, err);
|
|
313
343
|
}
|
|
314
344
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
catch (err) {
|
|
330
|
-
console.warn(`[channel] identity resolution failed for ${platform}; continuing without userKey`, err);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
const message = { ...msgFromTurn(turn), userKey };
|
|
334
|
-
const thread = makeThread(adapter, turn.replyTarget, turn.conversationKey, { platform, userKey, message });
|
|
345
|
+
const identityContext = identityContextFromIngress(platform, turn, "message");
|
|
346
|
+
const user = await resolveChannelUser(opts.identifyUser, identityContext);
|
|
347
|
+
const message = {
|
|
348
|
+
...msgFromTurn(turn),
|
|
349
|
+
user,
|
|
350
|
+
actor: identityContext.actor,
|
|
351
|
+
};
|
|
352
|
+
const thread = makeThread(adapter, turn.replyTarget, turn.conversationKey, {
|
|
353
|
+
platform,
|
|
354
|
+
message,
|
|
355
|
+
user,
|
|
356
|
+
actor: identityContext.actor,
|
|
357
|
+
});
|
|
335
358
|
const handlers = turn.operation.mentioned
|
|
336
359
|
? mentionHandlers.length > 0
|
|
337
360
|
? mentionHandlers
|
|
@@ -396,19 +419,27 @@ export function createChannel(opts) {
|
|
|
396
419
|
console.warn(`[channel] dedup check failed for ${platform}; processing without dedup`, err);
|
|
397
420
|
}
|
|
398
421
|
}
|
|
399
|
-
const
|
|
400
|
-
const
|
|
422
|
+
const identityContext = identityContextFromIngress(platform, evt, "interaction");
|
|
423
|
+
const canonicalUser = await resolveChannelUser(opts.identifyUser, identityContext);
|
|
424
|
+
const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey, {
|
|
425
|
+
platform,
|
|
426
|
+
user: canonicalUser,
|
|
427
|
+
actor: identityContext.actor,
|
|
428
|
+
interactionActionId: evt.id,
|
|
429
|
+
});
|
|
401
430
|
const ctx = {
|
|
402
431
|
thread,
|
|
403
432
|
message: {
|
|
404
433
|
text: "",
|
|
405
|
-
user,
|
|
434
|
+
user: canonicalUser,
|
|
435
|
+
actor: identityContext.actor,
|
|
406
436
|
ref: evt.messageRef ?? { id: "" },
|
|
407
437
|
platform,
|
|
408
438
|
},
|
|
409
439
|
action: { id: evt.id, value: evt.value },
|
|
410
|
-
values: {},
|
|
411
|
-
user,
|
|
440
|
+
values: evt.values ?? {},
|
|
441
|
+
user: canonicalUser,
|
|
442
|
+
actor: identityContext.actor,
|
|
412
443
|
platform,
|
|
413
444
|
};
|
|
414
445
|
const openModal = makeOpenModal(adapter, evt.replyTarget, evt.triggerId);
|
|
@@ -458,7 +489,13 @@ export function createChannel(opts) {
|
|
|
458
489
|
const command = commandHandlers.get(normalizeCommandName(cmd.command));
|
|
459
490
|
if (!command)
|
|
460
491
|
return; // unregistered command → skip
|
|
461
|
-
const
|
|
492
|
+
const identityContext = identityContextFromIngress(platform, cmd, "command");
|
|
493
|
+
const user = await resolveChannelUser(opts.identifyUser, identityContext);
|
|
494
|
+
const thread = makeThread(adapter, cmd.replyTarget, cmd.conversationKey, {
|
|
495
|
+
platform,
|
|
496
|
+
user,
|
|
497
|
+
actor: identityContext.actor,
|
|
498
|
+
});
|
|
462
499
|
// Resolve typed options from any structured args the surface supplied
|
|
463
500
|
// (e.g. Discord); text-only surfaces (Slack) leave `options` empty and
|
|
464
501
|
// the handler reads `text`.
|
|
@@ -473,7 +510,8 @@ export function createChannel(opts) {
|
|
|
473
510
|
command: normalizeCommandName(cmd.command),
|
|
474
511
|
text: cmd.text,
|
|
475
512
|
options,
|
|
476
|
-
user
|
|
513
|
+
user,
|
|
514
|
+
actor: identityContext.actor,
|
|
477
515
|
platform,
|
|
478
516
|
};
|
|
479
517
|
const openModal = makeOpenModal(adapter, cmd.replyTarget, cmd.triggerId);
|
|
@@ -481,13 +519,35 @@ export function createChannel(opts) {
|
|
|
481
519
|
ctx.openModal = openModal;
|
|
482
520
|
await command.handler(ctx);
|
|
483
521
|
},
|
|
522
|
+
async onWelcome(evt) {
|
|
523
|
+
const platform = ingressPlatform(adapter, evt);
|
|
524
|
+
if (evt.eventId && !adapter.skipIngressDedup) {
|
|
525
|
+
const dupKey = `evt:${platform}:${evt.eventId}`;
|
|
526
|
+
try {
|
|
527
|
+
if (await store.dedup.seen(dupKey, cfg.dedupTtl ?? 300_000))
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
console.warn(`[channel] dedup check failed for ${platform}; processing welcome without dedup`, err);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const identityContext = identityContextFromIngress(platform, evt, "welcome");
|
|
535
|
+
const user = await resolveChannelUser(opts.identifyUser, identityContext);
|
|
536
|
+
const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey, { platform, user, actor: identityContext.actor });
|
|
537
|
+
for (const h of welcomeHandlers) {
|
|
538
|
+
await h({ thread, user, actor: identityContext.actor, platform });
|
|
539
|
+
}
|
|
540
|
+
},
|
|
484
541
|
async onThreadStarted(evt) {
|
|
485
542
|
// The adapter has already applied its static defaults (greeting /
|
|
486
543
|
// prompts) before emitting this, so handlers layer on top and never
|
|
487
544
|
// race. Zero handlers → no-op.
|
|
488
|
-
const
|
|
545
|
+
const platform = ingressPlatform(adapter, evt);
|
|
546
|
+
const identityContext = identityContextFromIngress(platform, evt, "thread-start");
|
|
547
|
+
const user = await resolveChannelUser(opts.identifyUser, identityContext);
|
|
548
|
+
const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey, { platform, user, actor: identityContext.actor });
|
|
489
549
|
for (const h of threadStartedHandlers)
|
|
490
|
-
await h({ thread, user:
|
|
550
|
+
await h({ thread, user, actor: identityContext.actor });
|
|
491
551
|
},
|
|
492
552
|
async onReaction(evt) {
|
|
493
553
|
// Normalize by source platform, falling back to adapter identity for
|
|
@@ -497,14 +557,21 @@ export function createChannel(opts) {
|
|
|
497
557
|
? normalizeEmoji(evt.rawEmoji, sourcePlatform)
|
|
498
558
|
: undefined;
|
|
499
559
|
const value = normalized ?? evt.rawEmoji;
|
|
500
|
-
const
|
|
560
|
+
const identityContext = identityContextFromIngress(sourcePlatform, evt, "reaction");
|
|
561
|
+
const user = await resolveChannelUser(opts.identifyUser, identityContext);
|
|
562
|
+
const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey, {
|
|
563
|
+
platform: sourcePlatform,
|
|
564
|
+
user,
|
|
565
|
+
actor: identityContext.actor,
|
|
566
|
+
});
|
|
501
567
|
// Prefer the adapter's update-capable ref; fall back to the bare id.
|
|
502
568
|
const messageRef = evt.messageRef ?? { id: evt.messageId };
|
|
503
569
|
const reactionEvt = {
|
|
504
570
|
emoji: value,
|
|
505
571
|
rawEmoji: evt.rawEmoji,
|
|
506
572
|
added: evt.added,
|
|
507
|
-
user
|
|
573
|
+
user,
|
|
574
|
+
actor: identityContext.actor,
|
|
508
575
|
messageId: evt.messageId,
|
|
509
576
|
messageRef,
|
|
510
577
|
threadId: evt.threadId,
|
|
@@ -527,7 +594,8 @@ export function createChannel(opts) {
|
|
|
527
594
|
emoji: value,
|
|
528
595
|
rawEmoji: evt.rawEmoji,
|
|
529
596
|
added: evt.added,
|
|
530
|
-
user
|
|
597
|
+
user,
|
|
598
|
+
actor: identityContext.actor,
|
|
531
599
|
messageId: evt.messageId,
|
|
532
600
|
thread,
|
|
533
601
|
messageRef,
|
|
@@ -538,15 +606,20 @@ export function createChannel(opts) {
|
|
|
538
606
|
const handler = modalSubmitHandlers.get(evt.callbackId);
|
|
539
607
|
if (!handler)
|
|
540
608
|
return; // unregistered → closes
|
|
609
|
+
const identityContext = identityContextFromIngress(evt.platform, evt, "modal-submit");
|
|
610
|
+
const user = await resolveChannelUser(opts.identifyUser, identityContext);
|
|
541
611
|
const thread = evt.conversationKey !== undefined && evt.replyTarget !== undefined
|
|
542
612
|
? makeThread(adapter, evt.replyTarget, evt.conversationKey, {
|
|
543
613
|
platform: evt.platform,
|
|
614
|
+
user,
|
|
615
|
+
actor: identityContext.actor,
|
|
544
616
|
})
|
|
545
617
|
: undefined;
|
|
546
618
|
const result = await handler({
|
|
547
619
|
callbackId: evt.callbackId,
|
|
548
620
|
values: evt.values,
|
|
549
|
-
user
|
|
621
|
+
user,
|
|
622
|
+
actor: identityContext.actor,
|
|
550
623
|
thread,
|
|
551
624
|
privateMetadata: evt.privateMetadata,
|
|
552
625
|
raw: evt.raw,
|
|
@@ -557,9 +630,12 @@ export function createChannel(opts) {
|
|
|
557
630
|
const handler = modalCloseHandlers.get(evt.callbackId);
|
|
558
631
|
if (!handler)
|
|
559
632
|
return;
|
|
633
|
+
const identityContext = identityContextFromIngress(evt.platform, evt, "modal-close");
|
|
634
|
+
const user = await resolveChannelUser(opts.identifyUser, identityContext);
|
|
560
635
|
await handler({
|
|
561
636
|
callbackId: evt.callbackId,
|
|
562
|
-
user
|
|
637
|
+
user,
|
|
638
|
+
actor: identityContext.actor,
|
|
563
639
|
privateMetadata: evt.privateMetadata,
|
|
564
640
|
raw: evt.raw,
|
|
565
641
|
});
|
|
@@ -568,7 +644,6 @@ export function createChannel(opts) {
|
|
|
568
644
|
}
|
|
569
645
|
const channel = {
|
|
570
646
|
name: opts.name,
|
|
571
|
-
...(opts.provider !== undefined ? { provider: opts.provider } : {}),
|
|
572
647
|
...(opts.showToolStatus !== undefined
|
|
573
648
|
? { showToolStatus: opts.showToolStatus }
|
|
574
649
|
: {}),
|
|
@@ -590,11 +665,13 @@ export function createChannel(opts) {
|
|
|
590
665
|
return transcripts;
|
|
591
666
|
},
|
|
592
667
|
ɵruntime: {
|
|
668
|
+
enableIntelligenceMemory() {
|
|
669
|
+
intelligenceMemoryAvailable = true;
|
|
670
|
+
},
|
|
593
671
|
addAdapter(adapter) {
|
|
594
672
|
if (started) {
|
|
595
673
|
throw new Error("channel.ɵruntime.addAdapter must be called before channel.ɵruntime.start()");
|
|
596
674
|
}
|
|
597
|
-
assertExclusive([...adapters, adapter]);
|
|
598
675
|
adapters.push(adapter);
|
|
599
676
|
},
|
|
600
677
|
async start() {
|
|
@@ -610,7 +687,6 @@ export function createChannel(opts) {
|
|
|
610
687
|
throw new Error(`channel "${opts.name ?? "(unnamed)"}" must register onMention or onMessage before activation`);
|
|
611
688
|
}
|
|
612
689
|
started = true;
|
|
613
|
-
assertExclusive(adapters);
|
|
614
690
|
// Resolve persistence now that all adapters (including any attached via
|
|
615
691
|
// addAdapter) are known, then build the transcript store, action
|
|
616
692
|
// registry, and register components against it.
|
|
@@ -623,7 +699,11 @@ export function createChannel(opts) {
|
|
|
623
699
|
});
|
|
624
700
|
telemetry = tel;
|
|
625
701
|
const registryInstance = new ActionRegistry({
|
|
626
|
-
store: opts.actionStore ??
|
|
702
|
+
store: opts.actionStore ??
|
|
703
|
+
kvActionStore(backend, {
|
|
704
|
+
defaultTtlMs: cfg.actionRetentionMs ?? 7 * 24 * 60 * 60 * 1000,
|
|
705
|
+
}),
|
|
706
|
+
retentionMs: cfg.actionRetentionMs ?? 7 * 24 * 60 * 60 * 1000,
|
|
627
707
|
});
|
|
628
708
|
registry = registryInstance;
|
|
629
709
|
for (const c of opts.components ?? []) {
|
|
@@ -644,7 +724,6 @@ export function createChannel(opts) {
|
|
|
644
724
|
commandsCount: commandHandlers.size,
|
|
645
725
|
contextCount: context.length,
|
|
646
726
|
transcripts: !!cfg.transcripts,
|
|
647
|
-
identity: !!cfg.identity,
|
|
648
727
|
});
|
|
649
728
|
// Isolate per-adapter startup failures: one adapter rejecting (e.g.
|
|
650
729
|
// Telegram's setMyCommands rejecting a hyphenated command name, a revoked
|
|
@@ -669,6 +748,66 @@ export function createChannel(opts) {
|
|
|
669
748
|
startedPlatforms.push(platform);
|
|
670
749
|
}
|
|
671
750
|
});
|
|
751
|
+
const managedFailureIndex = startResults.findIndex((result, index) => adapters[index]?.__intelligenceChannel === true &&
|
|
752
|
+
result.status === "rejected");
|
|
753
|
+
const stopAdapters = async (indexes, reason) => {
|
|
754
|
+
const rollbackResults = await Promise.allSettled(indexes.map((index) => new Promise((resolve, reject) => {
|
|
755
|
+
let settled = false;
|
|
756
|
+
const timeout = setTimeout(() => {
|
|
757
|
+
if (settled)
|
|
758
|
+
return;
|
|
759
|
+
settled = true;
|
|
760
|
+
reject(new Error(`stop timed out after ${ADAPTER_ROLLBACK_TIMEOUT_MS}ms`));
|
|
761
|
+
}, ADAPTER_ROLLBACK_TIMEOUT_MS);
|
|
762
|
+
void Promise.resolve()
|
|
763
|
+
.then(() => adapters[index].stop())
|
|
764
|
+
.then(() => {
|
|
765
|
+
if (settled)
|
|
766
|
+
return;
|
|
767
|
+
settled = true;
|
|
768
|
+
clearTimeout(timeout);
|
|
769
|
+
resolve();
|
|
770
|
+
}, (error) => {
|
|
771
|
+
if (settled)
|
|
772
|
+
return;
|
|
773
|
+
settled = true;
|
|
774
|
+
clearTimeout(timeout);
|
|
775
|
+
reject(error);
|
|
776
|
+
});
|
|
777
|
+
})));
|
|
778
|
+
rollbackResults.forEach((result, resultIndex) => {
|
|
779
|
+
if (result.status === "rejected") {
|
|
780
|
+
const adapterIndex = indexes[resultIndex];
|
|
781
|
+
console.error(`[channel] adapter "${adapters[adapterIndex].platform}" failed to stop ${reason}:`, result.reason);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
};
|
|
785
|
+
const allAdapterIndexes = adapters.map((_adapter, index) => index);
|
|
786
|
+
const rejectedAdapterIndexes = startResults.flatMap((result, index) => result.status === "rejected" ? [index] : []);
|
|
787
|
+
if (managedFailureIndex >= 0) {
|
|
788
|
+
// Managed activation must never report online merely because a
|
|
789
|
+
// coexisting direct adapter started. Roll back every attempted
|
|
790
|
+
// adapter, including the rejecting managed adapter, because start()
|
|
791
|
+
// may have acquired resources before it failed.
|
|
792
|
+
await stopAdapters(allAdapterIndexes, "after managed activation failed");
|
|
793
|
+
started = false;
|
|
794
|
+
throw new Error(`channel "${opts.name ?? "(unnamed)"}" failed to start its managed Intelligence adapter`);
|
|
795
|
+
}
|
|
796
|
+
// A channel that has adapters but where NONE started is dead — surface an
|
|
797
|
+
// error so the runtime reports status "error", not a false "online". A
|
|
798
|
+
// PARTIAL start (>=1 adapter live) still counts as started. Reset the
|
|
799
|
+
// `started` guard so a caller can retry after fixing the misconfiguration.
|
|
800
|
+
if (adapters.length > 0 && startedPlatforms.length === 0) {
|
|
801
|
+
await stopAdapters(allAdapterIndexes, "after all adapters failed to start");
|
|
802
|
+
started = false;
|
|
803
|
+
throw new Error(`channel "${opts.name ?? "(unnamed)"}" failed to start: all ${failedPlatforms.length} adapter(s) failed to connect (${failedPlatforms.join(", ")}) — see the logged errors above`);
|
|
804
|
+
}
|
|
805
|
+
if (rejectedAdapterIndexes.length > 0) {
|
|
806
|
+
// A rejecting start may still have acquired a listener or provider
|
|
807
|
+
// client. Release only those failed adapters while preserving the
|
|
808
|
+
// healthy adapters that make this a valid degraded start.
|
|
809
|
+
await stopAdapters(rejectedAdapterIndexes, "after its start failed");
|
|
810
|
+
}
|
|
672
811
|
if (startedPlatforms.length > 0) {
|
|
673
812
|
tel.capture("oss.channel.started", {
|
|
674
813
|
platforms: startedPlatforms,
|
|
@@ -681,23 +820,18 @@ export function createChannel(opts) {
|
|
|
681
820
|
toolsCount: toolMap.size,
|
|
682
821
|
});
|
|
683
822
|
}
|
|
684
|
-
// A channel that has adapters but where NONE started is dead — surface an
|
|
685
|
-
// error so the runtime reports status "error", not a false "online". A
|
|
686
|
-
// PARTIAL start (>=1 adapter live) still counts as started. Reset the
|
|
687
|
-
// `started` guard so a caller can retry after fixing the misconfiguration.
|
|
688
|
-
if (adapters.length > 0 && startedPlatforms.length === 0) {
|
|
689
|
-
started = false;
|
|
690
|
-
throw new Error(`channel "${opts.name ?? "(unnamed)"}" failed to start: all ${failedPlatforms.length} adapter(s) failed to connect (${failedPlatforms.join(", ")}) — see the logged errors above`);
|
|
691
|
-
}
|
|
692
823
|
// Hand declared commands to adapters that register them up front (e.g.
|
|
693
|
-
// Discord); adapters without `registerCommands` are skipped.
|
|
694
|
-
//
|
|
824
|
+
// Discord); adapters without `registerCommands` are skipped. Never call
|
|
825
|
+
// into adapters whose start failed and whose resources were just
|
|
826
|
+
// cleaned up. Per-adapter failures are isolated the same way as start().
|
|
695
827
|
const commandSpecs = [...commandHandlers.values()].map(toCommandSpec);
|
|
696
828
|
if (commandSpecs.length > 0) {
|
|
697
|
-
const
|
|
698
|
-
registerResults.
|
|
829
|
+
const startedAdapterIndexes = startResults.flatMap((result, index) => result.status === "fulfilled" ? [index] : []);
|
|
830
|
+
const registerResults = await Promise.allSettled(startedAdapterIndexes.map((index) => adapters[index].registerCommands?.(commandSpecs)));
|
|
831
|
+
registerResults.forEach((r, resultIndex) => {
|
|
699
832
|
if (r.status === "rejected") {
|
|
700
|
-
|
|
833
|
+
const adapterIndex = startedAdapterIndexes[resultIndex];
|
|
834
|
+
console.error(`[channel] adapter "${adapters[adapterIndex].platform}" failed to register commands:`, r.reason);
|
|
701
835
|
}
|
|
702
836
|
});
|
|
703
837
|
}
|
|
@@ -710,7 +844,7 @@ export function createChannel(opts) {
|
|
|
710
844
|
started = false;
|
|
711
845
|
// Isolate per-adapter shutdown failures: one adapter's stop() rejecting
|
|
712
846
|
// must not prevent the others from being stopped.
|
|
713
|
-
const stopResults = await Promise.allSettled(adapters.map((a) => a.stop()));
|
|
847
|
+
const stopResults = await Promise.allSettled(adapters.map((a) => Promise.resolve().then(() => a.stop())));
|
|
714
848
|
stopResults.forEach((r, i) => {
|
|
715
849
|
if (r.status === "rejected") {
|
|
716
850
|
console.error(`[channel] adapter "${adapters[i].platform}" failed to stop:`, r.reason);
|
|
@@ -728,6 +862,9 @@ export function createChannel(opts) {
|
|
|
728
862
|
onMessage(h) {
|
|
729
863
|
messageHandlers.push(h);
|
|
730
864
|
},
|
|
865
|
+
onWelcome(h) {
|
|
866
|
+
welcomeHandlers.push(h);
|
|
867
|
+
},
|
|
731
868
|
onThreadStarted(h) {
|
|
732
869
|
threadStartedHandlers.push(h);
|
|
733
870
|
},
|