@xmoxmo/bncr 0.6.0 → 0.6.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.
Files changed (35) hide show
  1. package/README.md +8 -0
  2. package/dist/index.js +2 -0
  3. package/index.ts +1 -0
  4. package/package.json +4 -1
  5. package/scripts/check-pack.mjs +41 -1
  6. package/scripts/selfcheck.mjs +4 -1
  7. package/src/bootstrap/register-runtime-gateway.ts +1 -0
  8. package/src/bootstrap/register-runtime.ts +1 -0
  9. package/src/channel.ts +66 -5
  10. package/src/core/types.ts +41 -0
  11. package/src/messaging/inbound/context-facts.ts +88 -58
  12. package/src/messaging/inbound/{group-history.ts → conversation-history.ts} +92 -25
  13. package/src/messaging/inbound/dispatch.ts +90 -47
  14. package/src/messaging/inbound/native-command.ts +1 -1
  15. package/src/messaging/inbound/outbound-replay-cache.ts +312 -0
  16. package/src/messaging/inbound/scene-admin.ts +24 -9
  17. package/src/messaging/inbound/turn-context.ts +105 -33
  18. package/src/plugin/ack-outbox-runtime-group.ts +2 -0
  19. package/src/plugin/bridge-call.ts +168 -0
  20. package/src/plugin/bridge-outbox-facade.ts +88 -1
  21. package/src/plugin/channel-components.ts +10 -5
  22. package/src/plugin/channel-plugin-bridge-group.ts +43 -1
  23. package/src/plugin/channel-plugin-surface-group.ts +82 -7
  24. package/src/plugin/channel-runtime-builders-delivery.ts +4 -2
  25. package/src/plugin/channel-runtime-types.ts +34 -8
  26. package/src/plugin/channel-send.ts +12 -19
  27. package/src/plugin/client-rpc-runtime.ts +129 -0
  28. package/src/plugin/gateway-methods.ts +1 -0
  29. package/src/plugin/inbound-acceptance.ts +9 -24
  30. package/src/plugin/inbound-handlers.ts +8 -4
  31. package/src/plugin/inbound-surface-handlers-group.ts +7 -2
  32. package/src/plugin/outbox-ack-outcome.ts +2 -0
  33. package/src/plugin/scene-registry.ts +4 -4
  34. package/src/plugin/state-store.ts +153 -29
  35. package/src/plugin/state-transient-runtime-group.ts +10 -3
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createChannelHistoryWindow,
3
- DEFAULT_GROUP_HISTORY_LIMIT,
3
+ DEFAULT_GROUP_HISTORY_LIMIT as DEFAULT_HISTORY_LIMIT,
4
4
  type HistoryEntry,
5
5
  type HistoryMediaEntry,
6
6
  } from 'openclaw/plugin-sdk/reply-history';
@@ -10,9 +10,10 @@ import type { ParsedInbound } from './dispatch-prep.ts';
10
10
 
11
11
  export type BncrHistoryEntry = HistoryEntry & {
12
12
  senderId?: string;
13
+ role?: 'user' | 'assistant' | 'system';
13
14
  };
14
15
 
15
- export type BncrGroupHistoryMap = Map<string, BncrHistoryEntry[]>;
16
+ export type BncrConversationHistoryMap = Map<string, BncrHistoryEntry[]>;
16
17
 
17
18
  type BncrHistoryMediaKind = NonNullable<HistoryMediaEntry['kind']>;
18
19
 
@@ -22,16 +23,35 @@ function normalizeTextBody(value: string): string {
22
23
  .trim();
23
24
  }
24
25
 
25
- export function buildBncrGroupHistoryKey(parsed: ParsedInbound): string | null {
26
- if (parsed.peer.kind !== 'group') return null;
26
+ export function buildBncrConversationHistoryKey(parsed: ParsedInbound): string | null {
27
27
  const platform = String(parsed.platform || '').trim();
28
- const groupId = String(parsed.groupId || '').trim();
29
- if (!platform || !groupId || groupId === '0') return null;
30
- return `${platform}:${groupId}`;
28
+ if (!platform) return null;
29
+ if (parsed.peer.kind === 'group') {
30
+ const groupId = String(parsed.groupId || '').trim();
31
+ if (!groupId || groupId === '0') return null;
32
+ return `${platform}:${groupId}`;
33
+ }
34
+ const userId = String(parsed.userId || '').trim();
35
+ if (!userId || userId === '0') return null;
36
+ return `${platform}:${userId}`;
37
+ }
38
+
39
+ export function buildBncrConversationHistoryKeyFromRoute(args: {
40
+ platform?: string;
41
+ groupId?: string;
42
+ userId?: string;
43
+ }): string | null {
44
+ const platform = String(args.platform || '').trim();
45
+ if (!platform) return null;
46
+ const groupId = String(args.groupId || '0').trim() || '0';
47
+ const userId = String(args.userId || '0').trim() || '0';
48
+ if (groupId !== '0') return `${platform}:${groupId}`;
49
+ if (userId !== '0') return `${platform}:${userId}`;
50
+ return null;
31
51
  }
32
52
 
33
- export function recordBncrPendingGroupText(args: {
34
- historyMap: BncrGroupHistoryMap;
53
+ export function recordBncrPendingConversationText(args: {
54
+ historyMap: BncrConversationHistoryMap;
35
55
  parsed: ParsedInbound;
36
56
  senderDisplayName: string;
37
57
  senderId: string;
@@ -43,8 +63,8 @@ export function recordBncrPendingGroupText(args: {
43
63
  Number.isFinite(args.historyLimit) &&
44
64
  args.historyLimit >= 0
45
65
  ? Math.floor(args.historyLimit)
46
- : DEFAULT_GROUP_HISTORY_LIMIT;
47
- const historyKey = buildBncrGroupHistoryKey(args.parsed);
66
+ : DEFAULT_HISTORY_LIMIT;
67
+ const historyKey = buildBncrConversationHistoryKey(args.parsed);
48
68
  const body = normalizeTextBody(args.bodyText);
49
69
  if (!historyKey || !body || args.parsed.msgType !== 'text') return;
50
70
  const msgId = String(args.parsed.msgId || '').trim();
@@ -61,6 +81,7 @@ export function recordBncrPendingGroupText(args: {
61
81
  body,
62
82
  timestamp: Date.now(),
63
83
  messageId: args.parsed.msgId,
84
+ role: 'user',
64
85
  },
65
86
  });
66
87
  }
@@ -88,8 +109,8 @@ function buildBncrHistoryMediaBody(kind: BncrHistoryMediaKind): string {
88
109
  return `<media:${kind}>`;
89
110
  }
90
111
 
91
- export async function recordBncrPendingGroupMedia(args: {
92
- historyMap: BncrGroupHistoryMap;
112
+ export async function recordBncrPendingConversationMedia(args: {
113
+ historyMap: BncrConversationHistoryMap;
93
114
  parsed: ParsedInbound;
94
115
  senderDisplayName: string;
95
116
  senderId: string;
@@ -107,8 +128,8 @@ export async function recordBncrPendingGroupMedia(args: {
107
128
  Number.isFinite(args.historyLimit) &&
108
129
  args.historyLimit >= 0
109
130
  ? Math.floor(args.historyLimit)
110
- : DEFAULT_GROUP_HISTORY_LIMIT;
111
- const historyKey = buildBncrGroupHistoryKey(args.parsed);
131
+ : DEFAULT_HISTORY_LIMIT;
132
+ const historyKey = buildBncrConversationHistoryKey(args.parsed);
112
133
  if (!historyKey || args.parsed.msgType === 'text') return;
113
134
  const normalizedMediaItems = Array.isArray(args.mediaItems) ? args.mediaItems : [];
114
135
  const itemKinds = normalizedMediaItems
@@ -142,6 +163,7 @@ export async function recordBncrPendingGroupMedia(args: {
142
163
  body,
143
164
  timestamp: Date.now(),
144
165
  messageId: args.parsed.msgId,
166
+ role: 'user',
145
167
  },
146
168
  });
147
169
  return;
@@ -160,6 +182,7 @@ export async function recordBncrPendingGroupMedia(args: {
160
182
  body,
161
183
  timestamp: Date.now(),
162
184
  messageId: args.parsed.msgId,
185
+ role: 'user',
163
186
  media: normalizedMediaItems.map(
164
187
  (item) =>
165
188
  ({
@@ -173,12 +196,52 @@ export async function recordBncrPendingGroupMedia(args: {
173
196
  });
174
197
  }
175
198
 
199
+ export function recordBncrBotReply(args: {
200
+ historyMap: BncrConversationHistoryMap;
201
+ historyKey: string;
202
+ sender: string;
203
+ senderId?: string;
204
+ body: string;
205
+ timestamp?: number;
206
+ messageId?: string;
207
+ media?: HistoryMediaEntry[];
208
+ historyLimit?: number;
209
+ }) {
210
+ const limit =
211
+ typeof args.historyLimit === 'number' &&
212
+ Number.isFinite(args.historyLimit) &&
213
+ args.historyLimit >= 0
214
+ ? Math.floor(args.historyLimit)
215
+ : DEFAULT_HISTORY_LIMIT;
216
+ const body = normalizeTextBody(args.body);
217
+ if (!body) return;
218
+ const msgId = String(args.messageId || '').trim();
219
+ if (msgId) {
220
+ const existing = args.historyMap.get(args.historyKey) || [];
221
+ if (existing.some((e) => e.messageId === msgId)) return;
222
+ }
223
+ createChannelHistoryWindow({ historyMap: args.historyMap }).record({
224
+ historyKey: args.historyKey,
225
+ limit,
226
+ entry: {
227
+ sender: args.sender,
228
+ ...(args.senderId ? { senderId: args.senderId } : {}),
229
+ body,
230
+ timestamp: args.timestamp ?? Date.now(),
231
+ ...(args.messageId ? { messageId: args.messageId } : {}),
232
+ role: 'assistant',
233
+ ...(Array.isArray(args.media) && args.media.length > 0 ? { media: args.media } : {}),
234
+ },
235
+ });
236
+ }
237
+
176
238
  function cloneBncrHistoryEntry(entry: BncrHistoryEntry): BncrHistoryEntry {
177
239
  return {
178
240
  sender: entry.sender,
179
241
  ...(entry.messageId ? { messageId: entry.messageId } : {}),
180
242
  ...(typeof entry.timestamp === 'number' ? { timestamp: entry.timestamp } : {}),
181
243
  ...(entry.senderId ? { senderId: entry.senderId } : {}),
244
+ ...(entry.role ? { role: entry.role } : {}),
182
245
  body: entry.body,
183
246
  ...(Array.isArray(entry.media)
184
247
  ? {
@@ -193,8 +256,8 @@ function cloneBncrHistoryEntry(entry: BncrHistoryEntry): BncrHistoryEntry {
193
256
  };
194
257
  }
195
258
 
196
- export function readBncrPendingGroupHistorySnapshot(args: {
197
- historyMap: BncrGroupHistoryMap;
259
+ export function readBncrPendingConversationHistorySnapshot(args: {
260
+ historyMap: BncrConversationHistoryMap;
198
261
  parsed: ParsedInbound;
199
262
  historyLimit?: number;
200
263
  }): BncrHistoryEntry[] {
@@ -203,15 +266,19 @@ export function readBncrPendingGroupHistorySnapshot(args: {
203
266
  Number.isFinite(args.historyLimit) &&
204
267
  args.historyLimit >= 0
205
268
  ? Math.floor(args.historyLimit)
206
- : DEFAULT_GROUP_HISTORY_LIMIT;
207
- const historyKey = buildBncrGroupHistoryKey(args.parsed);
269
+ : DEFAULT_HISTORY_LIMIT;
270
+ const historyKey = buildBncrConversationHistoryKey(args.parsed);
208
271
  if (!historyKey) return [];
209
272
  const entries = args.historyMap.get(historyKey) || [];
210
273
  if (entries.length === 0) return [];
211
- return entries.slice(-limit).map(cloneBncrHistoryEntry);
274
+ return entries
275
+ .slice()
276
+ .sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0))
277
+ .slice(-limit)
278
+ .map(cloneBncrHistoryEntry);
212
279
  }
213
280
 
214
- export function buildBncrGroupContextFromEntries(args: {
281
+ export function buildBncrConversationContextFromEntries(args: {
215
282
  api: BncrInboundApi;
216
283
  entries: readonly BncrHistoryEntry[];
217
284
  channelLabel: string;
@@ -244,8 +311,8 @@ export function collectBncrHistoryMediaFromEntries(args: {
244
311
  }): HistoryMediaEntry[] {
245
312
  return args.entries.flatMap((entry) => (Array.isArray(entry.media) ? entry.media : []));
246
313
  }
247
- export function clearBncrPendingGroupHistory(args: {
248
- historyMap: BncrGroupHistoryMap;
314
+ export function clearBncrPendingConversationHistory(args: {
315
+ historyMap: BncrConversationHistoryMap;
249
316
  parsed: ParsedInbound;
250
317
  historyLimit?: number;
251
318
  }) {
@@ -254,8 +321,8 @@ export function clearBncrPendingGroupHistory(args: {
254
321
  Number.isFinite(args.historyLimit) &&
255
322
  args.historyLimit >= 0
256
323
  ? Math.floor(args.historyLimit)
257
- : DEFAULT_GROUP_HISTORY_LIMIT;
258
- const historyKey = buildBncrGroupHistoryKey(args.parsed);
324
+ : DEFAULT_HISTORY_LIMIT;
325
+ const historyKey = buildBncrConversationHistoryKey(args.parsed);
259
326
  if (!historyKey) return;
260
327
  createChannelHistoryWindow({ historyMap: args.historyMap }).clear({
261
328
  historyKey,
@@ -1,4 +1,4 @@
1
- import { DEFAULT_GROUP_HISTORY_LIMIT } from 'openclaw/plugin-sdk/reply-history';
1
+ import { DEFAULT_GROUP_HISTORY_LIMIT as DEFAULT_HISTORY_LIMIT } from 'openclaw/plugin-sdk/reply-history';
2
2
  import { emitBncrLogLine } from '../../core/logging.ts';
3
3
  import type { BncrSceneRecord } from '../../plugin/channel-runtime-types.ts';
4
4
  import { buildSceneKey } from '../../plugin/scene-registry.ts';
@@ -10,6 +10,15 @@ import type {
10
10
  BncrInboundLogger,
11
11
  BncrRememberSessionRoute,
12
12
  } from './contracts.ts';
13
+ import {
14
+ type BncrConversationHistoryMap,
15
+ type BncrHistoryEntry,
16
+ buildBncrConversationHistoryKey,
17
+ clearBncrPendingConversationHistory,
18
+ readBncrPendingConversationHistorySnapshot,
19
+ recordBncrPendingConversationMedia,
20
+ recordBncrPendingConversationText,
21
+ } from './conversation-history.ts';
13
22
  import {
14
23
  assertInboundMediaBase64Size,
15
24
  decodeInboundMediaBase64,
@@ -19,14 +28,10 @@ import {
19
28
  resolveBncrInboundConversation,
20
29
  } from './dispatch-prep.ts';
21
30
  import {
22
- type BncrGroupHistoryMap,
23
- type BncrHistoryEntry,
24
- buildBncrGroupHistoryKey,
25
- clearBncrPendingGroupHistory,
26
- readBncrPendingGroupHistorySnapshot,
27
- recordBncrPendingGroupMedia,
28
- recordBncrPendingGroupText,
29
- } from './group-history.ts';
31
+ type BncrOutboundReplayCache,
32
+ clearBncrOutboundReplay,
33
+ readBncrOutboundReplaySnapshot,
34
+ } from './outbound-replay-cache.ts';
30
35
  import { runBncrInboundReplyDispatch } from './reply-dispatch.ts';
31
36
  import { buildBncrInboundTurnContext } from './turn-context.ts';
32
37
 
@@ -47,7 +52,8 @@ export async function dispatchBncrInbound(params: {
47
52
  shouldAccumulate?: boolean;
48
53
  resolvedAgentId?: string;
49
54
  sceneRegistry: Map<string, BncrSceneRecord>;
50
- groupHistories: BncrGroupHistoryMap;
55
+ conversationHistories: BncrConversationHistoryMap;
56
+ outboundReplayCache?: BncrOutboundReplayCache;
51
57
  defaultAdminAgentId: string;
52
58
  defaultPublicAgentId: string;
53
59
  now: () => number;
@@ -67,7 +73,8 @@ export async function dispatchBncrInbound(params: {
67
73
  shouldAccumulate = initialShouldDispatch,
68
74
  resolvedAgentId,
69
75
  sceneRegistry = new Map(),
70
- groupHistories = new Map(),
76
+ conversationHistories = new Map(),
77
+ outboundReplayCache = new Map(),
71
78
  defaultAdminAgentId,
72
79
  defaultPublicAgentId,
73
80
  now,
@@ -137,31 +144,29 @@ export async function dispatchBncrInbound(params: {
137
144
  );
138
145
  }
139
146
  const sceneHistoryLimit =
140
- parsed.peer.kind === 'group'
141
- ? (sceneRegistry.get(buildSceneKey(parsed))?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT)
142
- : DEFAULT_GROUP_HISTORY_LIMIT;
147
+ sceneRegistry.get(buildSceneKey(parsed))?.historyLimit ?? DEFAULT_HISTORY_LIMIT;
143
148
  const resolvedHistoryLimit =
144
149
  typeof sceneHistoryLimit === 'number' &&
145
150
  Number.isFinite(sceneHistoryLimit) &&
146
151
  sceneHistoryLimit >= 0
147
152
  ? Math.floor(sceneHistoryLimit)
148
- : DEFAULT_GROUP_HISTORY_LIMIT;
153
+ : DEFAULT_HISTORY_LIMIT;
149
154
 
150
155
  let silentHistoryFlush = false;
151
156
  let pendingHistoryEntries: BncrHistoryEntry[] = [];
152
- if (!shouldDispatch && shouldAccumulate) {
153
- const historyKey = buildBncrGroupHistoryKey(parsed);
157
+ if (shouldAccumulate) {
158
+ const historyKey = buildBncrConversationHistoryKey(parsed);
154
159
  // Record the message first so overflow context includes it
155
- recordBncrPendingGroupText({
156
- historyMap: groupHistories,
160
+ recordBncrPendingConversationText({
161
+ historyMap: conversationHistories,
157
162
  parsed,
158
163
  senderDisplayName,
159
164
  senderId: senderIdForContext,
160
165
  bodyText: rawBody,
161
166
  historyLimit: resolvedHistoryLimit,
162
167
  });
163
- await recordBncrPendingGroupMedia({
164
- historyMap: groupHistories,
168
+ await recordBncrPendingConversationMedia({
169
+ historyMap: conversationHistories,
165
170
  parsed,
166
171
  senderDisplayName,
167
172
  senderId: senderIdForContext,
@@ -170,42 +175,86 @@ export async function dispatchBncrInbound(params: {
170
175
  mediaContentType: mediaContentType || mimeType,
171
176
  historyLimit: resolvedHistoryLimit,
172
177
  });
173
- if (historyKey && parsed.peer.kind === 'group') {
174
- const entries = groupHistories.get(historyKey);
178
+ if (historyKey) {
179
+ const entries = conversationHistories.get(historyKey);
175
180
  if (entries && resolvedHistoryLimit > 0 && entries.length >= resolvedHistoryLimit) {
176
181
  const sceneForHistory = sceneRegistry.get(buildSceneKey(parsed));
177
182
  const historyForce = sceneForHistory?.historyForce !== false;
178
183
  if (historyForce) {
179
- pendingHistoryEntries = readBncrPendingGroupHistorySnapshot({
180
- historyMap: groupHistories,
181
- parsed,
182
- historyLimit: resolvedHistoryLimit,
183
- });
184
- clearBncrPendingGroupHistory({
185
- historyMap: groupHistories,
186
- parsed,
187
- historyLimit: resolvedHistoryLimit,
188
- });
189
- shouldDispatch = true;
190
184
  silentHistoryFlush = true;
185
+ shouldDispatch = true;
191
186
  }
192
187
  }
193
188
  }
194
189
  }
195
190
 
196
- if (shouldDispatch && !silentHistoryFlush && parsed.peer.kind === 'group') {
197
- pendingHistoryEntries = readBncrPendingGroupHistorySnapshot({
198
- historyMap: groupHistories,
191
+ if (shouldDispatch) {
192
+ pendingHistoryEntries = readBncrPendingConversationHistorySnapshot({
193
+ historyMap: conversationHistories,
199
194
  parsed,
200
195
  historyLimit: resolvedHistoryLimit,
201
196
  });
202
197
  if (pendingHistoryEntries.length > 0) {
203
- clearBncrPendingGroupHistory({
204
- historyMap: groupHistories,
198
+ clearBncrPendingConversationHistory({
199
+ historyMap: conversationHistories,
205
200
  parsed,
206
201
  historyLimit: resolvedHistoryLimit,
207
202
  });
208
203
  }
204
+ const legacyOutboundEntries = readBncrOutboundReplaySnapshot({
205
+ cache: outboundReplayCache,
206
+ conversationHistories,
207
+ parsed,
208
+ accountId,
209
+ excludeMessageId: msgId,
210
+ });
211
+ clearBncrOutboundReplay({
212
+ cache: outboundReplayCache,
213
+ parsed,
214
+ accountId,
215
+ });
216
+ const knownMessageIds = new Set(
217
+ pendingHistoryEntries
218
+ .map((entry) => entry.messageId)
219
+ .filter((messageId): messageId is string => Boolean(messageId)),
220
+ );
221
+ for (const entry of legacyOutboundEntries) {
222
+ if (entry.messageId && knownMessageIds.has(entry.messageId)) continue;
223
+ if (entry.messageId) knownMessageIds.add(entry.messageId);
224
+ pendingHistoryEntries.push({
225
+ sender: entry.sender,
226
+ ...(entry.senderId ? { senderId: entry.senderId } : {}),
227
+ body: entry.body,
228
+ ...(typeof entry.timestamp === 'number' ? { timestamp: entry.timestamp } : {}),
229
+ ...(entry.messageId ? { messageId: entry.messageId } : {}),
230
+ role: 'assistant',
231
+ ...(Array.isArray(entry.media)
232
+ ? {
233
+ media: entry.media.map((item) => ({
234
+ path: item.path,
235
+ contentType: item.contentType,
236
+ kind: item.kind,
237
+ messageId: item.messageId,
238
+ })),
239
+ }
240
+ : {}),
241
+ });
242
+ }
243
+ pendingHistoryEntries.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
244
+ if (resolvedHistoryLimit > 0 && pendingHistoryEntries.length > resolvedHistoryLimit) {
245
+ const currentMessageId = String(msgId || '').trim();
246
+ const currentEntry = currentMessageId
247
+ ? pendingHistoryEntries.find((entry) => entry.messageId === currentMessageId)
248
+ : undefined;
249
+ pendingHistoryEntries = pendingHistoryEntries.slice(-resolvedHistoryLimit);
250
+ if (currentEntry && !pendingHistoryEntries.includes(currentEntry)) {
251
+ const withoutCurrent = pendingHistoryEntries.filter((entry) => entry !== currentEntry);
252
+ pendingHistoryEntries = [
253
+ ...withoutCurrent.slice(-(resolvedHistoryLimit - 1)),
254
+ currentEntry,
255
+ ];
256
+ }
257
+ }
209
258
  }
210
259
 
211
260
  const ctxPayload = await buildBncrInboundTurnContext({
@@ -223,7 +272,7 @@ export async function dispatchBncrInbound(params: {
223
272
  historyLimit: resolvedHistoryLimit,
224
273
  resolution,
225
274
  prepared,
226
- groupHistories,
275
+ conversationHistories,
227
276
  shouldDispatch,
228
277
  silentHistoryFlush,
229
278
  pendingHistoryEntries,
@@ -250,13 +299,7 @@ export async function dispatchBncrInbound(params: {
250
299
  enqueueFromReply,
251
300
  });
252
301
 
253
- if (shouldDispatch && pendingHistoryEntries.length === 0) {
254
- clearBncrPendingGroupHistory({
255
- historyMap: groupHistories,
256
- parsed,
257
- historyLimit: resolvedHistoryLimit,
258
- });
259
- }
302
+ scheduleSave();
260
303
 
261
304
  return {
262
305
  accountId,
@@ -55,7 +55,7 @@ const BNCR_HELP_TEXT = [
55
55
  ' • /bncr list pending [filters...]',
56
56
  ' • /bncr list scenes [filters...]',
57
57
  '',
58
- '📋 Group history',
58
+ '📋 Conversation history',
59
59
  ' • /bncr history-help',
60
60
  ' • /bncr history-limit [<number>|clear] [<SceneId>]',
61
61
  ' • /bncr history-force on|off|clear [<SceneId>]',