pi-roundtable 0.31.0 → 0.31.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.31.2] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `PeerBotEntry.yields`: a host leaves to a listed peer bot a person's message that addresses both. When a person mentions or replies to the host and in the same message mentions that bot or replies to its message, where the entry lists the bot and not in a direct conversation, the host starts no turn and logs `message left to a peer bot`; observers still hear the message. Only the peer answers, and it hands over by mentioning the host, whose answer to a listed peer is unchanged. Set it on one side only: two hosts that yield to each other both stay silent.
|
|
13
|
+
- `InboundMessage.mentionedUserIds?` (every user the message mentions, the bot among them) and `InboundMessage.reference.authorId?` (who wrote the replied-to message), both set by the Discord surface.
|
|
14
|
+
|
|
15
|
+
## [0.31.1] - 2026-11-01
|
|
16
|
+
|
|
17
|
+
- Release in lockstep with pi-roundtable-web 0.31.1 (the animated scene's progress strip names the member at work and folds to one line); no core behavior changes.
|
|
18
|
+
|
|
8
19
|
## [0.31.0] - 2026-11-01
|
|
9
20
|
|
|
10
21
|
### Added
|
package/docs/plugins.md
CHANGED
|
@@ -2868,6 +2868,12 @@ The router, not each claim, applies the list: for a message from a listed bot it
|
|
|
2868
2868
|
A channel answers at most `maxRounds` of the listed bots' messages in a row, counted when a claim takes the message; a person's message in the channel starts the count over, and the messages after the cap are ignored (logged at info).
|
|
2869
2869
|
Channel context is unchanged: its window still starts after the assistant's last post and other bots' messages are still cut at `botMessageChars`.
|
|
2870
2870
|
|
|
2871
|
+
When a person addresses two bots in one message, both would answer. An entry with `yields: true` makes the host leave such a message to that peer: a person's message (not in a direct conversation) that mentions or replies to the host and also mentions the peer or replies to its message, in a channel the entry covers, starts no turn here (logged at info as `message left to a peer bot`; observers still hear it). The peer answers alone and hands over by mentioning the host, which answers a listed peer as usual. Set it on one host only, and scope the entry with `guilds` or `channels` to where the peer actually answers; two hosts that yield to each other both stay silent. The router reads `InboundMessage.mentionedUserIds` (every user the message mentions) and `InboundMessage.reference.authorId` (who wrote the replied-to message), which the Discord surface sets.
|
|
2872
|
+
|
|
2873
|
+
```ts
|
|
2874
|
+
peerBots: { allow: [{ id: "800000000000000002", channels: ["700000000000000001"], yields: true }] },
|
|
2875
|
+
```
|
|
2876
|
+
|
|
2871
2877
|
The agent server answers a peer that mentions the agent or replies to it, like a guest. A listed peer's `speaker` is resolved like a person's, so a claim that checks only `speaker` answers it too. A claim of your own opts in where it once checked `!message.authorIsBot`, with `answersAuthor(message)` from `pi-roundtable/kit`, which is true for a person and for a flagged peer that mentions the bot or replies to it:
|
|
2872
2878
|
|
|
2873
2879
|
```ts
|
package/package.json
CHANGED
|
@@ -91,6 +91,11 @@ export interface InboundMessage {
|
|
|
91
91
|
*/
|
|
92
92
|
integration?: { id: string; own: boolean };
|
|
93
93
|
mentionsBot: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Every user the message mentions, the bot among them, in the surface's ids; absent when the
|
|
96
|
+
* surface does not report them. The router reads it for `peerBots` entries that yield.
|
|
97
|
+
*/
|
|
98
|
+
mentionedUserIds?: readonly string[];
|
|
94
99
|
/** The message replies to one of the bot's messages. */
|
|
95
100
|
repliesToBot: boolean;
|
|
96
101
|
/** Message text with the bot mention removed. */
|
|
@@ -100,6 +105,8 @@ export interface InboundMessage {
|
|
|
100
105
|
reference?: {
|
|
101
106
|
text: string;
|
|
102
107
|
attachments: AttachmentRef[];
|
|
108
|
+
/** The surface's id of the referenced message's author, when the surface reports it. */
|
|
109
|
+
authorId?: string;
|
|
103
110
|
/** The name a webhook posted the referenced message under, such as an agent's. */
|
|
104
111
|
webhookName?: string;
|
|
105
112
|
};
|
|
@@ -30,6 +30,7 @@ function referenceOf(referenced: Message, mention: RegExp) {
|
|
|
30
30
|
.replace(mention, "")
|
|
31
31
|
.trim(),
|
|
32
32
|
attachments: attachmentRefs(referenced),
|
|
33
|
+
authorId: referenced.author.id,
|
|
33
34
|
...(referenced.webhookId
|
|
34
35
|
? { webhookName: referenced.author.username }
|
|
35
36
|
: {}),
|
|
@@ -121,6 +122,7 @@ export async function toInbound(
|
|
|
121
122
|
...(thread ? { thread } : {}),
|
|
122
123
|
...(message.guildId ? { space: message.guildId } : {}),
|
|
123
124
|
mentionsBot: message.mentions.users.has(botId),
|
|
125
|
+
mentionedUserIds: [...message.mentions.users.keys()],
|
|
124
126
|
repliesToBot: referenced?.author.id === botId,
|
|
125
127
|
text: withEmbeds(message.content, message.embeds)
|
|
126
128
|
.replace(mention, "")
|
|
@@ -131,6 +131,14 @@ export class ChannelRouter implements ConversationPort {
|
|
|
131
131
|
// A bot nobody lists is not heard, as it is not answered; a listed peer is, rounds or not.
|
|
132
132
|
if (!message.authorIsBot || message.peerBot || message.integration)
|
|
133
133
|
this.#options.observers?.notify(message);
|
|
134
|
+
const yieldedTo = peers?.yieldsTo(message);
|
|
135
|
+
if (yieldedTo) {
|
|
136
|
+
this.#options.logger.info(
|
|
137
|
+
{ channel: message.channel, bot: yieldedTo },
|
|
138
|
+
"message left to a peer bot: it also addresses a peer this host yields to",
|
|
139
|
+
);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
134
142
|
const started = await this.#inOrder(message.channel, async () => {
|
|
135
143
|
if (listed && peers?.exhausted(message.channel)) {
|
|
136
144
|
this.#options.logger.info(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
bool,
|
|
2
3
|
either,
|
|
3
4
|
type Field,
|
|
4
5
|
integer,
|
|
@@ -20,6 +21,14 @@ export interface PeerBotEntry {
|
|
|
20
21
|
guilds?: readonly string[];
|
|
21
22
|
/** Answer it only in these channels; left out, in every channel. */
|
|
22
23
|
channels?: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* Leave to this bot a person's message that addresses both: when a person mentions or replies to
|
|
26
|
+
* the host and in the same message mentions this bot or replies to it, the host starts no turn
|
|
27
|
+
* (observers still hear the message), so only this bot answers and it can hand over by mentioning
|
|
28
|
+
* the host. Where `guilds` and `channels` do not list the bot, the host does not yield to it.
|
|
29
|
+
* Set it on one side only: two hosts that yield to each other both stay silent.
|
|
30
|
+
*/
|
|
31
|
+
yields?: boolean;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
/**
|
|
@@ -58,6 +67,7 @@ const entryShape = shape({
|
|
|
58
67
|
id: snowflake,
|
|
59
68
|
guilds: optional(list(snowflake)),
|
|
60
69
|
channels: optional(list(snowflake)),
|
|
70
|
+
yields: optional(bool),
|
|
61
71
|
});
|
|
62
72
|
|
|
63
73
|
/** `discord.peerBots`, or the `peerBots` of the host's options. */
|
|
@@ -93,13 +103,35 @@ export class PeerBots {
|
|
|
93
103
|
/** Whether the host lists the bot that wrote this message, for this channel and server. */
|
|
94
104
|
lists(message: InboundMessage): boolean {
|
|
95
105
|
if (!message.authorIsBot || message.integration) return false;
|
|
106
|
+
return this.#listsHere(message.authorId, message) !== undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The peer bot a person's message addressed to the host is left to: one the host lists here with
|
|
111
|
+
* `yields` that the message also mentions or replies to; undefined when the host answers it.
|
|
112
|
+
*/
|
|
113
|
+
yieldsTo(message: InboundMessage): string | undefined {
|
|
114
|
+
// The peer cannot answer in a direct conversation, so the host never yields one.
|
|
115
|
+
if (message.authorIsBot || message.integration || message.isDirect)
|
|
116
|
+
return undefined;
|
|
117
|
+
if (!message.mentionsBot && !message.repliesToBot) return undefined;
|
|
118
|
+
const addressed = new Set(message.mentionedUserIds ?? []);
|
|
119
|
+
if (message.reference?.authorId) addressed.add(message.reference.authorId);
|
|
120
|
+
for (const id of addressed)
|
|
121
|
+
if (this.#listsHere(id, message)?.yields) return id;
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The bot's entry that covers this message's channel and server, preferring one that yields. */
|
|
126
|
+
#listsHere(botId: string, message: InboundMessage): PeerBotEntry | undefined {
|
|
96
127
|
const { surface, id: channelId } = parseChannelKey(message.channel);
|
|
97
|
-
if (surface !== this.#surface) return
|
|
98
|
-
|
|
128
|
+
if (surface !== this.#surface) return undefined;
|
|
129
|
+
const here = (this.#entries.get(botId) ?? []).filter(
|
|
99
130
|
(entry) =>
|
|
100
131
|
(!entry.guilds || entry.guilds.includes(message.space ?? "")) &&
|
|
101
132
|
(!entry.channels || entry.channels.includes(channelId)),
|
|
102
133
|
);
|
|
134
|
+
return here.find((entry) => entry.yields) ?? here[0];
|
|
103
135
|
}
|
|
104
136
|
|
|
105
137
|
/** Whether the channel has answered its rounds of peer bots since a person last posted. */
|