@songsid/agend 2.1.2-beta.45 → 2.1.2-beta.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fleet-manager.d.ts +14 -4
- package/dist/fleet-manager.js +91 -49
- package/dist/fleet-manager.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
63
63
|
private instanceWorldBinding;
|
|
64
64
|
private recentMessageIds;
|
|
65
65
|
private accessManager;
|
|
66
|
-
/** Primary world (
|
|
66
|
+
/** Primary world (channels[0]), independent of concurrent adapter startup order. */
|
|
67
67
|
get primaryWorld(): AdapterWorld | undefined;
|
|
68
68
|
readonly routing: RoutingEngine;
|
|
69
69
|
get routingTable(): Map<string, RouteTarget>;
|
|
@@ -223,12 +223,22 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
223
223
|
getChannelConfig(adapterId?: string): import("./types.js").ChannelConfig | undefined;
|
|
224
224
|
/** Get the group_id for an instance's bound adapter */
|
|
225
225
|
getGroupIdForInstance(name: string): string;
|
|
226
|
+
/** Configured primary adapter id. Never infer this from Map insertion order. */
|
|
227
|
+
private getPrimaryAdapterId;
|
|
228
|
+
/** Warn when a coordinator's adapter identity is ambiguous to the operator. */
|
|
229
|
+
private warnUnboundGeneralChannelIds;
|
|
230
|
+
/**
|
|
231
|
+
* Resolve the authoritative adapter identity for an instance.
|
|
232
|
+
* Fleet instances without channel_id and legacy Classic entries both belong
|
|
233
|
+
* to channels[0]. Runtime bindings remain available for external sessions.
|
|
234
|
+
*/
|
|
235
|
+
private getInstanceAdapterId;
|
|
226
236
|
/**
|
|
227
237
|
* Bind an instance to a specific world (the bot that answers for it).
|
|
228
238
|
* fromInbound=true (binding inferred from which adapter received a message)
|
|
229
|
-
* must not override a configured identity
|
|
230
|
-
*
|
|
231
|
-
*
|
|
239
|
+
* must not override a configured identity. Fleet instances use channel_id or
|
|
240
|
+
* channels[0]; Classic instances use their persisted adapter (or channels[0]
|
|
241
|
+
* for a legacy entry). Only external sessions may bind from inbound traffic.
|
|
232
242
|
*/
|
|
233
243
|
bindInstanceAdapter(name: string, adapterId: string, fromInbound?: boolean): void;
|
|
234
244
|
getInstanceStatus(name: string): "running" | "paused" | "stopped" | "crashed";
|
package/dist/fleet-manager.js
CHANGED
|
@@ -185,8 +185,11 @@ export class FleetManager {
|
|
|
185
185
|
// same guild both receive every message). Bounded FIFO of recent message keys.
|
|
186
186
|
recentMessageIds = new Set();
|
|
187
187
|
accessManager = null;
|
|
188
|
-
/** Primary world (
|
|
189
|
-
get primaryWorld() {
|
|
188
|
+
/** Primary world (channels[0]), independent of concurrent adapter startup order. */
|
|
189
|
+
get primaryWorld() {
|
|
190
|
+
const adapterId = this.getPrimaryAdapterId();
|
|
191
|
+
return adapterId ? this.worlds.get(adapterId) : undefined;
|
|
192
|
+
}
|
|
190
193
|
routing = new RoutingEngine();
|
|
191
194
|
get routingTable() { return this.routing.map; }
|
|
192
195
|
instanceIpcClients = new Map();
|
|
@@ -458,10 +461,20 @@ export class FleetManager {
|
|
|
458
461
|
if (!this.classicChannels)
|
|
459
462
|
return;
|
|
460
463
|
const channels = this.classicChannels.getAll();
|
|
464
|
+
// Classic's persisted adapter is authoritative. Legacy adapter-less rows
|
|
465
|
+
// deterministically belong to channels[0], never to whichever bot happens
|
|
466
|
+
// to deliver the first message after startup/reconnect.
|
|
467
|
+
for (const ch of channels) {
|
|
468
|
+
const adapterId = ch.adapterId ?? this.getPrimaryAdapterId();
|
|
469
|
+
if (adapterId)
|
|
470
|
+
this.instanceWorldBinding.set(ch.instanceName, adapterId);
|
|
471
|
+
}
|
|
461
472
|
// Always update adapter openChannels (including empty — clears stale entries on /stop)
|
|
462
473
|
for (const [adapterId, w] of this.worlds) {
|
|
463
474
|
if (typeof w.adapter?.setOpenChannels === "function") {
|
|
464
|
-
const owned = channels
|
|
475
|
+
const owned = channels
|
|
476
|
+
.filter(ch => (ch.adapterId ?? this.getPrimaryAdapterId()) === adapterId)
|
|
477
|
+
.map(ch => ch.channelId);
|
|
465
478
|
w.adapter.setOpenChannels(owned);
|
|
466
479
|
}
|
|
467
480
|
}
|
|
@@ -539,51 +552,88 @@ export class FleetManager {
|
|
|
539
552
|
}
|
|
540
553
|
/** Get the adapter bound to an instance, falling back to primary adapter */
|
|
541
554
|
getAdapterForInstance(name) {
|
|
542
|
-
const worldId = this.
|
|
555
|
+
const worldId = this.getInstanceAdapterId(name);
|
|
543
556
|
if (worldId)
|
|
544
557
|
return this.worlds.get(worldId)?.adapter ?? this.adapter;
|
|
545
558
|
return this.adapter;
|
|
546
559
|
}
|
|
547
560
|
/** Get the world for an instance */
|
|
548
561
|
getWorldForInstance(name) {
|
|
549
|
-
const worldId = this.
|
|
550
|
-
return worldId ? this.worlds.get(worldId) :
|
|
562
|
+
const worldId = this.getInstanceAdapterId(name);
|
|
563
|
+
return worldId ? this.worlds.get(worldId) : undefined;
|
|
551
564
|
}
|
|
552
565
|
/** Get channel config for a specific adapter (by id), falling back to primary */
|
|
553
566
|
getChannelConfig(adapterId) {
|
|
567
|
+
const channels = this.fleetConfig?.channels
|
|
568
|
+
?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
554
569
|
if (adapterId) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
570
|
+
return channels.find(ch => (ch.id ?? ch.type) === adapterId)
|
|
571
|
+
?? this.worlds.get(adapterId)?.channelConfig
|
|
572
|
+
?? channels[0];
|
|
558
573
|
}
|
|
559
|
-
return
|
|
574
|
+
return channels[0];
|
|
560
575
|
}
|
|
561
576
|
/** Get the group_id for an instance's bound adapter */
|
|
562
577
|
getGroupIdForInstance(name) {
|
|
563
|
-
const
|
|
564
|
-
|
|
578
|
+
const adapterId = this.getInstanceAdapterId(name);
|
|
579
|
+
const world = adapterId ? this.worlds.get(adapterId) : undefined;
|
|
580
|
+
return world?.groupId ?? String(this.getChannelConfig(adapterId)?.group_id ?? "");
|
|
581
|
+
}
|
|
582
|
+
/** Configured primary adapter id. Never infer this from Map insertion order. */
|
|
583
|
+
getPrimaryAdapterId() {
|
|
584
|
+
const primary = this.fleetConfig?.channels?.[0] ?? this.fleetConfig?.channel;
|
|
585
|
+
if (primary)
|
|
586
|
+
return primary.id ?? primary.type;
|
|
587
|
+
// Defensive compatibility for callers/tests that provide a live world but
|
|
588
|
+
// no channel config. Real multi-adapter fleets always have channels[].
|
|
589
|
+
return this.worlds.keys().next().value;
|
|
590
|
+
}
|
|
591
|
+
/** Warn when a coordinator's adapter identity is ambiguous to the operator. */
|
|
592
|
+
warnUnboundGeneralChannelIds(fleet) {
|
|
593
|
+
const channels = fleet.channels ?? (fleet.channel ? [fleet.channel] : []);
|
|
594
|
+
if (channels.length <= 1)
|
|
595
|
+
return;
|
|
596
|
+
const adapterIds = channels.map(ch => ch.id ?? ch.type);
|
|
597
|
+
for (const [name, config] of Object.entries(fleet.instances)) {
|
|
598
|
+
if (!config.general_topic || config.channel_id)
|
|
599
|
+
continue;
|
|
600
|
+
this.logger.warn({
|
|
601
|
+
instance: name,
|
|
602
|
+
defaultAdapter: adapterIds[0],
|
|
603
|
+
availableAdapters: adapterIds,
|
|
604
|
+
}, "General instance has no channel_id in a multi-channel fleet; defaulting to the first adapter. Set channel_id explicitly.");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Resolve the authoritative adapter identity for an instance.
|
|
609
|
+
* Fleet instances without channel_id and legacy Classic entries both belong
|
|
610
|
+
* to channels[0]. Runtime bindings remain available for external sessions.
|
|
611
|
+
*/
|
|
612
|
+
getInstanceAdapterId(name) {
|
|
613
|
+
const cfg = this.fleetConfig?.instances[name];
|
|
614
|
+
if (cfg)
|
|
615
|
+
return cfg.channel_id ?? this.getPrimaryAdapterId();
|
|
616
|
+
if (this.classicChannels?.getChannelIdByInstance(name) !== undefined) {
|
|
617
|
+
return this.classicChannels.getAdapterIdByInstance(name) ?? this.getPrimaryAdapterId();
|
|
618
|
+
}
|
|
619
|
+
return this.instanceWorldBinding.get(name) ?? this.getPrimaryAdapterId();
|
|
565
620
|
}
|
|
566
621
|
/**
|
|
567
622
|
* Bind an instance to a specific world (the bot that answers for it).
|
|
568
623
|
* fromInbound=true (binding inferred from which adapter received a message)
|
|
569
|
-
* must not override a configured identity
|
|
570
|
-
*
|
|
571
|
-
*
|
|
624
|
+
* must not override a configured identity. Fleet instances use channel_id or
|
|
625
|
+
* channels[0]; Classic instances use their persisted adapter (or channels[0]
|
|
626
|
+
* for a legacy entry). Only external sessions may bind from inbound traffic.
|
|
572
627
|
*/
|
|
573
628
|
bindInstanceAdapter(name, adapterId, fromInbound = false) {
|
|
574
|
-
const cfg = this.fleetConfig?.instances[name];
|
|
575
629
|
if (fromInbound) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
return;
|
|
582
|
-
if (cfg && !cfg.channel_id)
|
|
583
|
-
return; // fleet instance without explicit binding → use primary
|
|
584
|
-
// Classic instance: don't override an existing binding (authoritative from /start)
|
|
585
|
-
if (this.classicChannels?.getChannelIdByInstance(name) !== undefined && this.instanceWorldBinding.has(name))
|
|
630
|
+
const configuredId = this.getInstanceAdapterId(name);
|
|
631
|
+
if (this.fleetConfig?.instances[name]
|
|
632
|
+
|| this.classicChannels?.getChannelIdByInstance(name) !== undefined) {
|
|
633
|
+
if (configuredId)
|
|
634
|
+
this.instanceWorldBinding.set(name, configuredId);
|
|
586
635
|
return;
|
|
636
|
+
}
|
|
587
637
|
}
|
|
588
638
|
this.instanceWorldBinding.set(name, adapterId);
|
|
589
639
|
}
|
|
@@ -1335,6 +1385,7 @@ export class FleetManager {
|
|
|
1335
1385
|
this.rotateInboxes();
|
|
1336
1386
|
// Auto-create/adopt a general dispatcher — ONLY for the primary adapter.
|
|
1337
1387
|
const channelConfigs = fleet.channels ?? (fleet.channel ? [fleet.channel] : []);
|
|
1388
|
+
this.warnUnboundGeneralChannelIds(fleet);
|
|
1338
1389
|
const primaryAdapterId = channelConfigs[0] ? (channelConfigs[0].id ?? channelConfigs[0].type) : undefined;
|
|
1339
1390
|
const generalInstances = Object.entries(fleet.instances).filter(([, inst]) => inst.general_topic === true);
|
|
1340
1391
|
let generalsCreated = false;
|
|
@@ -1494,25 +1545,14 @@ export class FleetManager {
|
|
|
1494
1545
|
catch (err) {
|
|
1495
1546
|
this.logger.error({ err }, "startSharedAdapter failed — fleet continues without some adapters");
|
|
1496
1547
|
}
|
|
1497
|
-
// Bind
|
|
1498
|
-
//
|
|
1499
|
-
//
|
|
1500
|
-
|
|
1501
|
-
const channelConfigsForBind = fleet.channels ?? (fleet.channel ? [fleet.channel] : []);
|
|
1548
|
+
// Bind every fleet instance deterministically. Explicit channel_id wins;
|
|
1549
|
+
// otherwise channels[0] is authoritative. Do not infer identity from
|
|
1550
|
+
// concurrent adapter startup or whichever bot receives a message first.
|
|
1551
|
+
const primaryAdapterId = this.getPrimaryAdapterId();
|
|
1502
1552
|
for (const [name, config] of Object.entries(fleet.instances)) {
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
}
|
|
1507
|
-
if (!config.general_topic)
|
|
1508
|
-
continue;
|
|
1509
|
-
for (const ch of channelConfigsForBind) {
|
|
1510
|
-
const id = ch.id ?? ch.type;
|
|
1511
|
-
if (name.includes(id)) {
|
|
1512
|
-
this.bindInstanceAdapter(name, id);
|
|
1513
|
-
break;
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1553
|
+
const adapterId = config.channel_id ?? primaryAdapterId;
|
|
1554
|
+
if (adapterId)
|
|
1555
|
+
this.bindInstanceAdapter(name, adapterId);
|
|
1516
1556
|
}
|
|
1517
1557
|
// Guard against a stale/invalid general topic_id. An old auto-general
|
|
1518
1558
|
// could have written the TG-convention "1" for a Discord general; the DC
|
|
@@ -1522,7 +1562,7 @@ export class FleetManager {
|
|
|
1522
1562
|
for (const [name, cfg] of Object.entries(this.fleetConfig.instances)) {
|
|
1523
1563
|
if (!cfg.general_topic || cfg.topic_id == null)
|
|
1524
1564
|
continue;
|
|
1525
|
-
const adapterId = this.
|
|
1565
|
+
const adapterId = this.getInstanceAdapterId(name);
|
|
1526
1566
|
if (this.getChannelConfig(adapterId)?.type === "discord" && !/^\d{17,}$/.test(String(cfg.topic_id))) {
|
|
1527
1567
|
this.logger.warn({ name, topic_id: cfg.topic_id }, "Discord general topic_id is not a valid channel — unbinding to avoid a crash loop");
|
|
1528
1568
|
delete cfg.topic_id;
|
|
@@ -2075,7 +2115,9 @@ export class FleetManager {
|
|
|
2075
2115
|
this.probeCliEnvs();
|
|
2076
2116
|
this.adapter.on("started", safeHandler((username, userId) => {
|
|
2077
2117
|
this.logger.info(`Bot @${username} polling started. Ensure no other service is polling this bot token.`);
|
|
2078
|
-
|
|
2118
|
+
// Concurrent startup can insert a secondary world first. Update the
|
|
2119
|
+
// configured primary world, not Map insertion order.
|
|
2120
|
+
const w = this.worlds.get(adapterId);
|
|
2079
2121
|
if (w) {
|
|
2080
2122
|
w.botUsername = username;
|
|
2081
2123
|
if (userId)
|
|
@@ -4203,7 +4245,7 @@ export class FleetManager {
|
|
|
4203
4245
|
const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
4204
4246
|
if (!adapter)
|
|
4205
4247
|
return;
|
|
4206
|
-
const channelCfg = this.getChannelConfig(this.
|
|
4248
|
+
const channelCfg = this.getChannelConfig(this.getInstanceAdapterId(instanceName));
|
|
4207
4249
|
const groupId = channelCfg?.group_id;
|
|
4208
4250
|
// Fleet topic instance
|
|
4209
4251
|
const threadId = this.fleetConfig?.instances[instanceName]?.topic_id;
|
|
@@ -4328,7 +4370,7 @@ export class FleetManager {
|
|
|
4328
4370
|
// `channels:` worlds the primary `channel:` block is empty, so an instance
|
|
4329
4371
|
// with no world binding yet (fresh restart, cross-instance delegation)
|
|
4330
4372
|
// resolved group_id to undefined and the button silently never appeared.
|
|
4331
|
-
const adapterId = this.
|
|
4373
|
+
const adapterId = this.getInstanceAdapterId(instanceName);
|
|
4332
4374
|
const groupId = this.getGroupIdForInstance(instanceName) || undefined;
|
|
4333
4375
|
const topicId = this.fleetConfig?.instances[instanceName]?.topic_id;
|
|
4334
4376
|
let chatId;
|
|
@@ -4753,7 +4795,7 @@ export class FleetManager {
|
|
|
4753
4795
|
const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
4754
4796
|
if (!adapter)
|
|
4755
4797
|
return;
|
|
4756
|
-
const channelCfg = this.getChannelConfig(this.
|
|
4798
|
+
const channelCfg = this.getChannelConfig(this.getInstanceAdapterId(instanceName));
|
|
4757
4799
|
const groupId = channelCfg?.group_id;
|
|
4758
4800
|
if (!groupId)
|
|
4759
4801
|
return;
|