@xmoxmo/bncr 0.6.1 → 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.
- package/package.json +1 -1
- package/scripts/check-pack.mjs +1 -1
- package/scripts/selfcheck.mjs +1 -1
- package/src/channel.ts +16 -4
- package/src/messaging/inbound/context-facts.ts +88 -58
- package/src/messaging/inbound/{group-history.ts → conversation-history.ts} +92 -25
- package/src/messaging/inbound/dispatch.ts +73 -48
- package/src/messaging/inbound/native-command.ts +1 -1
- package/src/messaging/inbound/outbound-replay-cache.ts +82 -6
- package/src/messaging/inbound/scene-admin.ts +24 -9
- package/src/messaging/inbound/turn-context.ts +101 -37
- package/src/plugin/bridge-outbox-facade.ts +9 -0
- package/src/plugin/channel-components.ts +5 -5
- package/src/plugin/channel-runtime-builders-delivery.ts +2 -2
- package/src/plugin/channel-runtime-types.ts +9 -7
- package/src/plugin/inbound-acceptance.ts +9 -24
- package/src/plugin/inbound-handlers.ts +4 -4
- package/src/plugin/inbound-surface-handlers-group.ts +4 -2
- package/src/plugin/scene-registry.ts +4 -4
- package/src/plugin/state-store.ts +38 -34
- package/src/plugin/state-transient-runtime-group.ts +3 -3
|
@@ -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,
|
|
@@ -18,18 +27,8 @@ import {
|
|
|
18
27
|
prepareBncrInboundDispatch,
|
|
19
28
|
resolveBncrInboundConversation,
|
|
20
29
|
} from './dispatch-prep.ts';
|
|
21
|
-
import {
|
|
22
|
-
type BncrGroupHistoryMap,
|
|
23
|
-
type BncrHistoryEntry,
|
|
24
|
-
buildBncrGroupHistoryKey,
|
|
25
|
-
clearBncrPendingGroupHistory,
|
|
26
|
-
readBncrPendingGroupHistorySnapshot,
|
|
27
|
-
recordBncrPendingGroupMedia,
|
|
28
|
-
recordBncrPendingGroupText,
|
|
29
|
-
} from './group-history.ts';
|
|
30
30
|
import {
|
|
31
31
|
type BncrOutboundReplayCache,
|
|
32
|
-
type BncrOutboundReplayEntry,
|
|
33
32
|
clearBncrOutboundReplay,
|
|
34
33
|
readBncrOutboundReplaySnapshot,
|
|
35
34
|
} from './outbound-replay-cache.ts';
|
|
@@ -53,7 +52,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
53
52
|
shouldAccumulate?: boolean;
|
|
54
53
|
resolvedAgentId?: string;
|
|
55
54
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
56
|
-
|
|
55
|
+
conversationHistories: BncrConversationHistoryMap;
|
|
57
56
|
outboundReplayCache?: BncrOutboundReplayCache;
|
|
58
57
|
defaultAdminAgentId: string;
|
|
59
58
|
defaultPublicAgentId: string;
|
|
@@ -74,7 +73,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
74
73
|
shouldAccumulate = initialShouldDispatch,
|
|
75
74
|
resolvedAgentId,
|
|
76
75
|
sceneRegistry = new Map(),
|
|
77
|
-
|
|
76
|
+
conversationHistories = new Map(),
|
|
78
77
|
outboundReplayCache = new Map(),
|
|
79
78
|
defaultAdminAgentId,
|
|
80
79
|
defaultPublicAgentId,
|
|
@@ -145,31 +144,29 @@ export async function dispatchBncrInbound(params: {
|
|
|
145
144
|
);
|
|
146
145
|
}
|
|
147
146
|
const sceneHistoryLimit =
|
|
148
|
-
parsed
|
|
149
|
-
? (sceneRegistry.get(buildSceneKey(parsed))?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT)
|
|
150
|
-
: DEFAULT_GROUP_HISTORY_LIMIT;
|
|
147
|
+
sceneRegistry.get(buildSceneKey(parsed))?.historyLimit ?? DEFAULT_HISTORY_LIMIT;
|
|
151
148
|
const resolvedHistoryLimit =
|
|
152
149
|
typeof sceneHistoryLimit === 'number' &&
|
|
153
150
|
Number.isFinite(sceneHistoryLimit) &&
|
|
154
151
|
sceneHistoryLimit >= 0
|
|
155
152
|
? Math.floor(sceneHistoryLimit)
|
|
156
|
-
:
|
|
153
|
+
: DEFAULT_HISTORY_LIMIT;
|
|
157
154
|
|
|
158
155
|
let silentHistoryFlush = false;
|
|
159
156
|
let pendingHistoryEntries: BncrHistoryEntry[] = [];
|
|
160
|
-
if (
|
|
161
|
-
const historyKey =
|
|
157
|
+
if (shouldAccumulate) {
|
|
158
|
+
const historyKey = buildBncrConversationHistoryKey(parsed);
|
|
162
159
|
// Record the message first so overflow context includes it
|
|
163
|
-
|
|
164
|
-
historyMap:
|
|
160
|
+
recordBncrPendingConversationText({
|
|
161
|
+
historyMap: conversationHistories,
|
|
165
162
|
parsed,
|
|
166
163
|
senderDisplayName,
|
|
167
164
|
senderId: senderIdForContext,
|
|
168
165
|
bodyText: rawBody,
|
|
169
166
|
historyLimit: resolvedHistoryLimit,
|
|
170
167
|
});
|
|
171
|
-
await
|
|
172
|
-
historyMap:
|
|
168
|
+
await recordBncrPendingConversationMedia({
|
|
169
|
+
historyMap: conversationHistories,
|
|
173
170
|
parsed,
|
|
174
171
|
senderDisplayName,
|
|
175
172
|
senderId: senderIdForContext,
|
|
@@ -178,48 +175,35 @@ export async function dispatchBncrInbound(params: {
|
|
|
178
175
|
mediaContentType: mediaContentType || mimeType,
|
|
179
176
|
historyLimit: resolvedHistoryLimit,
|
|
180
177
|
});
|
|
181
|
-
if (historyKey
|
|
182
|
-
const entries =
|
|
178
|
+
if (historyKey) {
|
|
179
|
+
const entries = conversationHistories.get(historyKey);
|
|
183
180
|
if (entries && resolvedHistoryLimit > 0 && entries.length >= resolvedHistoryLimit) {
|
|
184
181
|
const sceneForHistory = sceneRegistry.get(buildSceneKey(parsed));
|
|
185
182
|
const historyForce = sceneForHistory?.historyForce !== false;
|
|
186
183
|
if (historyForce) {
|
|
187
|
-
pendingHistoryEntries = readBncrPendingGroupHistorySnapshot({
|
|
188
|
-
historyMap: groupHistories,
|
|
189
|
-
parsed,
|
|
190
|
-
historyLimit: resolvedHistoryLimit,
|
|
191
|
-
});
|
|
192
|
-
clearBncrPendingGroupHistory({
|
|
193
|
-
historyMap: groupHistories,
|
|
194
|
-
parsed,
|
|
195
|
-
historyLimit: resolvedHistoryLimit,
|
|
196
|
-
});
|
|
197
|
-
shouldDispatch = true;
|
|
198
184
|
silentHistoryFlush = true;
|
|
185
|
+
shouldDispatch = true;
|
|
199
186
|
}
|
|
200
187
|
}
|
|
201
188
|
}
|
|
202
189
|
}
|
|
203
190
|
|
|
204
|
-
if (shouldDispatch
|
|
205
|
-
pendingHistoryEntries =
|
|
206
|
-
historyMap:
|
|
191
|
+
if (shouldDispatch) {
|
|
192
|
+
pendingHistoryEntries = readBncrPendingConversationHistorySnapshot({
|
|
193
|
+
historyMap: conversationHistories,
|
|
207
194
|
parsed,
|
|
208
195
|
historyLimit: resolvedHistoryLimit,
|
|
209
196
|
});
|
|
210
197
|
if (pendingHistoryEntries.length > 0) {
|
|
211
|
-
|
|
212
|
-
historyMap:
|
|
198
|
+
clearBncrPendingConversationHistory({
|
|
199
|
+
historyMap: conversationHistories,
|
|
213
200
|
parsed,
|
|
214
201
|
historyLimit: resolvedHistoryLimit,
|
|
215
202
|
});
|
|
216
203
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
let outboundReplayEntries: BncrOutboundReplayEntry[] = [];
|
|
220
|
-
if (shouldDispatch && !silentHistoryFlush) {
|
|
221
|
-
outboundReplayEntries = readBncrOutboundReplaySnapshot({
|
|
204
|
+
const legacyOutboundEntries = readBncrOutboundReplaySnapshot({
|
|
222
205
|
cache: outboundReplayCache,
|
|
206
|
+
conversationHistories,
|
|
223
207
|
parsed,
|
|
224
208
|
accountId,
|
|
225
209
|
excludeMessageId: msgId,
|
|
@@ -229,6 +213,48 @@ export async function dispatchBncrInbound(params: {
|
|
|
229
213
|
parsed,
|
|
230
214
|
accountId,
|
|
231
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
|
+
}
|
|
232
258
|
}
|
|
233
259
|
|
|
234
260
|
const ctxPayload = await buildBncrInboundTurnContext({
|
|
@@ -246,11 +272,10 @@ export async function dispatchBncrInbound(params: {
|
|
|
246
272
|
historyLimit: resolvedHistoryLimit,
|
|
247
273
|
resolution,
|
|
248
274
|
prepared,
|
|
249
|
-
|
|
275
|
+
conversationHistories,
|
|
250
276
|
shouldDispatch,
|
|
251
277
|
silentHistoryFlush,
|
|
252
278
|
pendingHistoryEntries,
|
|
253
|
-
outboundReplayEntries,
|
|
254
279
|
});
|
|
255
280
|
|
|
256
281
|
await runBncrInboundReplyDispatch({
|
|
@@ -55,7 +55,7 @@ const BNCR_HELP_TEXT = [
|
|
|
55
55
|
' • /bncr list pending [filters...]',
|
|
56
56
|
' • /bncr list scenes [filters...]',
|
|
57
57
|
'',
|
|
58
|
-
'📋
|
|
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>]',
|
|
@@ -4,6 +4,12 @@ import type {
|
|
|
4
4
|
BncrOutboundReplayStatus,
|
|
5
5
|
OutboxEntry,
|
|
6
6
|
} from '../../core/types.ts';
|
|
7
|
+
import {
|
|
8
|
+
type BncrConversationHistoryMap,
|
|
9
|
+
buildBncrConversationHistoryKey,
|
|
10
|
+
buildBncrConversationHistoryKeyFromRoute,
|
|
11
|
+
recordBncrBotReply,
|
|
12
|
+
} from './conversation-history.ts';
|
|
7
13
|
import type { ParsedInbound } from './dispatch-prep.ts';
|
|
8
14
|
|
|
9
15
|
export type {
|
|
@@ -152,6 +158,8 @@ function isAppMsg(type: string | undefined, msg: string): boolean {
|
|
|
152
158
|
|
|
153
159
|
export function recordBncrOutboundReplay(args: {
|
|
154
160
|
cache: BncrOutboundReplayCache;
|
|
161
|
+
conversationHistories?: BncrConversationHistoryMap;
|
|
162
|
+
historyLimit?: number;
|
|
155
163
|
entry: OutboxEntry;
|
|
156
164
|
sender: string;
|
|
157
165
|
senderId?: string;
|
|
@@ -179,6 +187,38 @@ export function recordBncrOutboundReplay(args: {
|
|
|
179
187
|
? buildMediaBody('document')
|
|
180
188
|
: normalizeTextBody(rawBody) || (isMedia ? buildMediaBody(kind) : '');
|
|
181
189
|
if (!body) return;
|
|
190
|
+
const recordTimestamp = args.entry.lastPushAt ?? Date.now();
|
|
191
|
+
|
|
192
|
+
if (args.conversationHistories) {
|
|
193
|
+
const historyKey = buildBncrConversationHistoryKeyFromRoute({
|
|
194
|
+
platform: args.entry.route?.platform,
|
|
195
|
+
groupId: args.entry.route?.groupId,
|
|
196
|
+
userId: args.entry.route?.userId,
|
|
197
|
+
});
|
|
198
|
+
if (historyKey) {
|
|
199
|
+
recordBncrBotReply({
|
|
200
|
+
historyMap: args.conversationHistories,
|
|
201
|
+
historyKey,
|
|
202
|
+
sender: args.sender,
|
|
203
|
+
senderId: args.senderId,
|
|
204
|
+
body,
|
|
205
|
+
timestamp: recordTimestamp,
|
|
206
|
+
messageId: args.entry.messageId,
|
|
207
|
+
historyLimit: args.historyLimit,
|
|
208
|
+
...(mediaUrl
|
|
209
|
+
? {
|
|
210
|
+
media: [
|
|
211
|
+
{
|
|
212
|
+
path: mediaUrl,
|
|
213
|
+
kind,
|
|
214
|
+
messageId: args.entry.messageId,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
}
|
|
218
|
+
: {}),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
182
222
|
|
|
183
223
|
recordOutboundReplayEntry({
|
|
184
224
|
cache: args.cache,
|
|
@@ -187,7 +227,7 @@ export function recordBncrOutboundReplay(args: {
|
|
|
187
227
|
sender: args.sender,
|
|
188
228
|
...(args.senderId ? { senderId: args.senderId } : {}),
|
|
189
229
|
body,
|
|
190
|
-
timestamp:
|
|
230
|
+
timestamp: recordTimestamp,
|
|
191
231
|
messageId: args.entry.messageId,
|
|
192
232
|
accountId: args.entry.accountId,
|
|
193
233
|
sessionKey: args.entry.sessionKey,
|
|
@@ -213,16 +253,52 @@ export function recordBncrOutboundReplay(args: {
|
|
|
213
253
|
|
|
214
254
|
export function readBncrOutboundReplaySnapshot(args: {
|
|
215
255
|
cache: BncrOutboundReplayCache;
|
|
256
|
+
conversationHistories?: BncrConversationHistoryMap;
|
|
216
257
|
parsed: ParsedInbound;
|
|
217
258
|
accountId: string;
|
|
218
259
|
excludeMessageId?: string | null;
|
|
219
260
|
}): BncrOutboundReplayEntry[] {
|
|
220
|
-
const cacheKey = buildBncrOutboundReplayKey(args.parsed, args.accountId);
|
|
221
|
-
if (!cacheKey) return [];
|
|
222
261
|
const excludedMessageId = String(args.excludeMessageId || '').trim();
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
.
|
|
262
|
+
const cacheKey = buildBncrOutboundReplayKey(args.parsed, args.accountId);
|
|
263
|
+
const legacyEntries = cacheKey
|
|
264
|
+
? (args.cache.get(cacheKey) || [])
|
|
265
|
+
.filter((entry) => !excludedMessageId || entry.messageId !== excludedMessageId)
|
|
266
|
+
.map(cloneBncrOutboundReplayEntry)
|
|
267
|
+
: [];
|
|
268
|
+
if (!args.conversationHistories) return legacyEntries;
|
|
269
|
+
|
|
270
|
+
const historyKey = buildBncrConversationHistoryKey(args.parsed);
|
|
271
|
+
const historyEntries = historyKey
|
|
272
|
+
? (args.conversationHistories.get(historyKey) || [])
|
|
273
|
+
.filter((entry) => entry.role === 'assistant')
|
|
274
|
+
.filter((entry) => !excludedMessageId || entry.messageId !== excludedMessageId)
|
|
275
|
+
.map((entry) => ({
|
|
276
|
+
sender: entry.sender,
|
|
277
|
+
...(entry.senderId ? { senderId: entry.senderId } : {}),
|
|
278
|
+
body: entry.body,
|
|
279
|
+
...(typeof entry.timestamp === 'number' ? { timestamp: entry.timestamp } : {}),
|
|
280
|
+
...(entry.messageId ? { messageId: entry.messageId } : {}),
|
|
281
|
+
...(Array.isArray(entry.media)
|
|
282
|
+
? {
|
|
283
|
+
media: entry.media.map((item) => ({
|
|
284
|
+
path: item.path,
|
|
285
|
+
contentType: item.contentType,
|
|
286
|
+
kind: item.kind,
|
|
287
|
+
messageId: item.messageId,
|
|
288
|
+
})),
|
|
289
|
+
}
|
|
290
|
+
: {}),
|
|
291
|
+
}))
|
|
292
|
+
: [];
|
|
293
|
+
const knownMessageIds = new Set(
|
|
294
|
+
historyEntries
|
|
295
|
+
.map((entry) => entry.messageId)
|
|
296
|
+
.filter((messageId): messageId is string => Boolean(messageId)),
|
|
297
|
+
);
|
|
298
|
+
return [
|
|
299
|
+
...historyEntries,
|
|
300
|
+
...legacyEntries.filter((entry) => !entry.messageId || !knownMessageIds.has(entry.messageId)),
|
|
301
|
+
].sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
|
|
226
302
|
}
|
|
227
303
|
|
|
228
304
|
export function clearBncrOutboundReplay(args: {
|
|
@@ -49,7 +49,7 @@ const MODE_HELP_TEXT = [
|
|
|
49
49
|
].join('\n');
|
|
50
50
|
|
|
51
51
|
export const HISTORY_HELP_TEXT = [
|
|
52
|
-
'📋 Bncr
|
|
52
|
+
'📋 Bncr Conversation History Configuration',
|
|
53
53
|
'',
|
|
54
54
|
'Commands:',
|
|
55
55
|
' • /bncr history-limit <number> [<SceneId>]',
|
|
@@ -88,6 +88,22 @@ function resolveCurrentGroupSceneKey(parsed: ParsedInbound): string | null {
|
|
|
88
88
|
return `${platform}:${groupId}`;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function resolveCurrentSceneKey(parsed: ParsedInbound): string | null {
|
|
92
|
+
const platform = normalizeToken(parsed.platform);
|
|
93
|
+
if (!platform) return null;
|
|
94
|
+
if (parsed.peer.kind === 'group') {
|
|
95
|
+
const groupId = normalizeToken(parsed.groupId);
|
|
96
|
+
if (!groupId || groupId === '0') return null;
|
|
97
|
+
return `${platform}:${groupId}`;
|
|
98
|
+
}
|
|
99
|
+
if (parsed.peer.kind === 'direct') {
|
|
100
|
+
const userId = normalizeToken(parsed.userId);
|
|
101
|
+
if (!userId || userId === '0') return null;
|
|
102
|
+
return `${platform}:${userId}`;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdminCommand {
|
|
92
108
|
const args = splitArgs(command.argsText);
|
|
93
109
|
switch (command.command) {
|
|
@@ -340,8 +356,7 @@ function formatSceneDetailsLine(scene: BncrSceneRecord): string {
|
|
|
340
356
|
const historyLimitVal = scene.historyLimit;
|
|
341
357
|
const historyForceVal = scene.historyForce;
|
|
342
358
|
const hasNonDefaultHistory =
|
|
343
|
-
|
|
344
|
-
((typeof historyLimitVal === 'number' && historyLimitVal !== 50) || historyForceVal === false);
|
|
359
|
+
(typeof historyLimitVal === 'number' && historyLimitVal !== 50) || historyForceVal === false;
|
|
345
360
|
const historyPart = hasNonDefaultHistory
|
|
346
361
|
? ` historyLimit=${historyLimitVal ?? 50} autoFlush=${historyForceVal !== false ? 'on' : 'off'}`
|
|
347
362
|
: '';
|
|
@@ -475,7 +490,7 @@ export function executeSceneAdminCommand(args: {
|
|
|
475
490
|
}
|
|
476
491
|
|
|
477
492
|
if (command.kind === 'history-limit-get') {
|
|
478
|
-
const qSceneKey = command.sceneKey ||
|
|
493
|
+
const qSceneKey = command.sceneKey || resolveCurrentSceneKey(parsed);
|
|
479
494
|
if (!qSceneKey) {
|
|
480
495
|
return { ok: true, text: 'Default history limit is 50.' };
|
|
481
496
|
}
|
|
@@ -485,9 +500,9 @@ export function executeSceneAdminCommand(args: {
|
|
|
485
500
|
}
|
|
486
501
|
|
|
487
502
|
if (command.kind === 'history-limit-set') {
|
|
488
|
-
const sSceneKey = command.sceneKey ||
|
|
503
|
+
const sSceneKey = command.sceneKey || resolveCurrentSceneKey(parsed);
|
|
489
504
|
if (!sSceneKey) {
|
|
490
|
-
return { ok: false, text: 'Current
|
|
505
|
+
return { ok: false, text: 'Current conversation shortcut requires a valid chat.' };
|
|
491
506
|
}
|
|
492
507
|
const sExisting = sceneRegistry.get(sSceneKey);
|
|
493
508
|
if (!sExisting) return { ok: false, text: `Scene not found: ${sSceneKey}` };
|
|
@@ -520,7 +535,7 @@ export function executeSceneAdminCommand(args: {
|
|
|
520
535
|
}
|
|
521
536
|
|
|
522
537
|
if (command.kind === 'history-force-get') {
|
|
523
|
-
const qSceneKey = command.sceneKey ||
|
|
538
|
+
const qSceneKey = command.sceneKey || resolveCurrentSceneKey(parsed);
|
|
524
539
|
if (!qSceneKey) {
|
|
525
540
|
return { ok: true, text: 'Default history auto flush is on.' };
|
|
526
541
|
}
|
|
@@ -534,9 +549,9 @@ export function executeSceneAdminCommand(args: {
|
|
|
534
549
|
}
|
|
535
550
|
|
|
536
551
|
if (command.kind === 'history-force-set') {
|
|
537
|
-
const sSceneKey = command.sceneKey ||
|
|
552
|
+
const sSceneKey = command.sceneKey || resolveCurrentSceneKey(parsed);
|
|
538
553
|
if (!sSceneKey) {
|
|
539
|
-
return { ok: false, text: 'Current
|
|
554
|
+
return { ok: false, text: 'Current conversation shortcut requires a valid chat.' };
|
|
540
555
|
}
|
|
541
556
|
const sExisting = sceneRegistry.get(sSceneKey);
|
|
542
557
|
if (!sExisting) return { ok: false, text: `Scene not found: ${sSceneKey}` };
|
|
@@ -9,20 +9,88 @@ import {
|
|
|
9
9
|
buildBncrStructuredContextFactsFromInboundParts,
|
|
10
10
|
} from './context-facts.ts';
|
|
11
11
|
import type { BncrInboundApi, BncrInboundConfig, BncrInboundContextPayload } from './contracts.ts';
|
|
12
|
+
import {
|
|
13
|
+
type BncrConversationHistoryMap,
|
|
14
|
+
type BncrHistoryEntry,
|
|
15
|
+
collectBncrHistoryMediaFromEntries,
|
|
16
|
+
} from './conversation-history.ts';
|
|
12
17
|
import type {
|
|
13
18
|
BncrInboundConversationResolution,
|
|
14
19
|
BncrPreparedInboundSessionContext,
|
|
15
20
|
ParsedInbound,
|
|
16
21
|
} from './dispatch-prep.ts';
|
|
17
|
-
import {
|
|
18
|
-
type BncrGroupHistoryMap,
|
|
19
|
-
type BncrHistoryEntry,
|
|
20
|
-
buildBncrGroupContextFromEntries,
|
|
21
|
-
collectBncrHistoryMediaFromEntries,
|
|
22
|
-
} from './group-history.ts';
|
|
23
|
-
import type { BncrOutboundReplayEntry } from './outbound-replay-cache.ts';
|
|
24
22
|
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
25
23
|
|
|
24
|
+
function dedupeBncrTurnMedia(entries: readonly HistoryMediaEntry[]): HistoryMediaEntry[] {
|
|
25
|
+
const seen = new Set<string>();
|
|
26
|
+
const result: HistoryMediaEntry[] = [];
|
|
27
|
+
for (const item of entries) {
|
|
28
|
+
const key = `${item.path ?? ''}:${item.messageId ?? ''}`;
|
|
29
|
+
if (key && seen.has(key)) continue;
|
|
30
|
+
seen.add(key);
|
|
31
|
+
result.push(item);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildBncrParticipantsMap(args: {
|
|
37
|
+
entries: readonly BncrHistoryEntry[];
|
|
38
|
+
parsed: ParsedInbound;
|
|
39
|
+
currentSenderId: string;
|
|
40
|
+
currentSenderName: string;
|
|
41
|
+
currentSenderUsername?: string;
|
|
42
|
+
currentSenderIsAdmin: boolean;
|
|
43
|
+
currentSenderIsOwner: boolean;
|
|
44
|
+
currentSenderIsAuthorized: boolean;
|
|
45
|
+
}): Record<
|
|
46
|
+
string,
|
|
47
|
+
{
|
|
48
|
+
name: string;
|
|
49
|
+
username?: string;
|
|
50
|
+
isBot?: boolean;
|
|
51
|
+
isAdmin?: boolean;
|
|
52
|
+
isOwner?: boolean;
|
|
53
|
+
isAuthorizedSender?: boolean;
|
|
54
|
+
role?: 'owner' | 'admin' | 'user';
|
|
55
|
+
displayName?: string;
|
|
56
|
+
}
|
|
57
|
+
> {
|
|
58
|
+
const map: Record<
|
|
59
|
+
string,
|
|
60
|
+
{
|
|
61
|
+
name: string;
|
|
62
|
+
username?: string;
|
|
63
|
+
isBot?: boolean;
|
|
64
|
+
isAdmin?: boolean;
|
|
65
|
+
isOwner?: boolean;
|
|
66
|
+
isAuthorizedSender?: boolean;
|
|
67
|
+
role?: 'owner' | 'admin' | 'user';
|
|
68
|
+
displayName?: string;
|
|
69
|
+
}
|
|
70
|
+
> = {};
|
|
71
|
+
map[args.currentSenderId] = {
|
|
72
|
+
name: args.currentSenderName,
|
|
73
|
+
...(args.currentSenderUsername ? { username: args.currentSenderUsername } : {}),
|
|
74
|
+
isBot: false,
|
|
75
|
+
...(args.currentSenderIsAdmin ? { isAdmin: true } : {}),
|
|
76
|
+
...(args.currentSenderIsOwner ? { isOwner: true } : {}),
|
|
77
|
+
...(args.currentSenderIsAuthorized ? { isAuthorizedSender: true } : {}),
|
|
78
|
+
role: args.currentSenderIsOwner ? 'owner' : args.currentSenderIsAdmin ? 'admin' : 'user',
|
|
79
|
+
displayName: args.currentSenderName,
|
|
80
|
+
};
|
|
81
|
+
for (const entry of args.entries) {
|
|
82
|
+
if (!entry.senderId || map[entry.senderId]) continue;
|
|
83
|
+
const isBot = entry.role === 'assistant';
|
|
84
|
+
map[entry.senderId] = {
|
|
85
|
+
name: entry.sender || entry.senderId,
|
|
86
|
+
...(isBot ? { isBot: true } : {}),
|
|
87
|
+
role: isBot ? 'admin' : 'user',
|
|
88
|
+
displayName: entry.sender || entry.senderId,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return map;
|
|
92
|
+
}
|
|
93
|
+
|
|
26
94
|
function parseSlashCommandName(body: string): string | undefined {
|
|
27
95
|
const raw = String(body || '').trim();
|
|
28
96
|
if (!raw.startsWith('/')) return undefined;
|
|
@@ -74,12 +142,11 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
74
142
|
bridgeSenderName?: string;
|
|
75
143
|
resolution: BncrInboundConversationResolution;
|
|
76
144
|
prepared: BncrPreparedInboundSessionContext;
|
|
77
|
-
|
|
145
|
+
conversationHistories: BncrConversationHistoryMap;
|
|
78
146
|
shouldDispatch: boolean;
|
|
79
147
|
historyLimit?: number;
|
|
80
148
|
silentHistoryFlush?: boolean;
|
|
81
149
|
pendingHistoryEntries?: readonly BncrHistoryEntry[];
|
|
82
|
-
outboundReplayEntries?: readonly BncrOutboundReplayEntry[];
|
|
83
150
|
}): BncrInboundContextPayload | Promise<BncrInboundContextPayload> {
|
|
84
151
|
const {
|
|
85
152
|
api,
|
|
@@ -100,17 +167,12 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
100
167
|
const pendingHistoryEntries = Array.isArray(args.pendingHistoryEntries)
|
|
101
168
|
? args.pendingHistoryEntries
|
|
102
169
|
: [];
|
|
103
|
-
const outboundReplayEntries = Array.isArray(args.outboundReplayEntries)
|
|
104
|
-
? args.outboundReplayEntries
|
|
105
|
-
: [];
|
|
106
170
|
const silentHistoryFlush = args.silentHistoryFlush === true;
|
|
107
|
-
const contextEntries =
|
|
108
|
-
? pendingHistoryEntries
|
|
109
|
-
: [...pendingHistoryEntries, ...outboundReplayEntries];
|
|
171
|
+
const contextEntries = pendingHistoryEntries;
|
|
110
172
|
const flushSenderId = 'bncr-history-system';
|
|
111
173
|
const flushSenderName = 'Bncr History System';
|
|
112
174
|
const flushRawBody =
|
|
113
|
-
'This message was automatically generated by the system for
|
|
175
|
+
'This message was automatically generated by the system for conversation context accumulation. Do not reply to the user. Output exactly NO_REPLY.';
|
|
114
176
|
const effectiveSenderId = silentHistoryFlush ? flushSenderId : senderIdForContext;
|
|
115
177
|
const effectiveSenderDisplayName = silentHistoryFlush ? flushSenderName : senderDisplayName;
|
|
116
178
|
const effectiveRawBody = silentHistoryFlush ? flushRawBody : prepared.rawBody;
|
|
@@ -137,17 +199,32 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
137
199
|
const structuredContextEntries = shouldDispatch
|
|
138
200
|
? contextEntries.map((entry) => ({
|
|
139
201
|
messageId: entry.messageId,
|
|
202
|
+
timestamp: entry.timestamp,
|
|
203
|
+
role: entry.role || 'user',
|
|
140
204
|
sender: entry.sender,
|
|
141
205
|
senderId: entry.senderId,
|
|
142
|
-
|
|
206
|
+
content: entry.body,
|
|
143
207
|
media: (entry.media || []).map((item: HistoryMediaEntry) => ({
|
|
208
|
+
type: item.kind || 'document',
|
|
144
209
|
path: item.path,
|
|
145
210
|
contentType: item.contentType,
|
|
146
|
-
kind: item.kind,
|
|
147
211
|
messageId: item.messageId,
|
|
148
212
|
})),
|
|
149
213
|
}))
|
|
150
214
|
: [];
|
|
215
|
+
const isGroupChat = parsed.isGroup === true;
|
|
216
|
+
const participants = isGroupChat
|
|
217
|
+
? buildBncrParticipantsMap({
|
|
218
|
+
entries: contextEntries,
|
|
219
|
+
parsed,
|
|
220
|
+
currentSenderId: effectiveSenderId,
|
|
221
|
+
currentSenderName: effectiveSenderDisplayName,
|
|
222
|
+
currentSenderUsername: parsed.userName,
|
|
223
|
+
currentSenderIsAdmin: parsed.isAdmin === true,
|
|
224
|
+
currentSenderIsOwner: senderIsOwner,
|
|
225
|
+
currentSenderIsAuthorized: senderIsAuthorized,
|
|
226
|
+
})
|
|
227
|
+
: null;
|
|
151
228
|
const structuredContextFacts = buildBncrStructuredContextFactsFromInboundParts({
|
|
152
229
|
channelId,
|
|
153
230
|
parsed,
|
|
@@ -163,7 +240,10 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
163
240
|
bridgeSenderName,
|
|
164
241
|
senderIsOwner,
|
|
165
242
|
senderIsAuthorized,
|
|
166
|
-
|
|
243
|
+
conversationHistoryContext: structuredContextEntries,
|
|
244
|
+
participants,
|
|
245
|
+
isGroupChat,
|
|
246
|
+
groupSubject: parsed.groupName,
|
|
167
247
|
});
|
|
168
248
|
const promptVisibleContextFacts = buildBncrPromptVisibleContextFacts(structuredContextFacts);
|
|
169
249
|
const supplementalUntrustedContext = [
|
|
@@ -184,30 +264,14 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
184
264
|
const platformWantsReply = parsed.shouldRespond === true;
|
|
185
265
|
const wasMentioned = parsed.isBotMentioned === true || platformWantsReply;
|
|
186
266
|
const isAuthorizedTextCommand = Boolean(ownerAllowFrom?.length);
|
|
187
|
-
const bodyForAgent =
|
|
188
|
-
shouldDispatch && contextEntries.length > 0
|
|
189
|
-
? buildBncrGroupContextFromEntries({
|
|
190
|
-
api,
|
|
191
|
-
entries: contextEntries,
|
|
192
|
-
channelLabel: resolution.canonicalTo,
|
|
193
|
-
currentTimestamp: Date.now(),
|
|
194
|
-
previousTimestamp: readBncrSessionUpdatedAt(api, {
|
|
195
|
-
storePath: prepared.storePath,
|
|
196
|
-
sessionKey: resolution.dispatchSessionKey,
|
|
197
|
-
}),
|
|
198
|
-
envelope: envelopeOptions,
|
|
199
|
-
currentMessage: typeof effectiveBody === 'string' ? effectiveBody : prepared.body,
|
|
200
|
-
})
|
|
201
|
-
: typeof effectiveBody === 'string'
|
|
202
|
-
? effectiveBody
|
|
203
|
-
: prepared.body;
|
|
267
|
+
const bodyForAgent = typeof effectiveBody === 'string' ? effectiveBody : prepared.body;
|
|
204
268
|
const currentTurnMedia = (silentHistoryFlush ? [] : prepared.mediaItems).map((item) => ({
|
|
205
269
|
path: item.path,
|
|
206
270
|
contentType: item.contentType,
|
|
207
271
|
kind: item.kind,
|
|
208
272
|
messageId: msgId ?? undefined,
|
|
209
273
|
}));
|
|
210
|
-
const media = [...contextMedia, ...currentTurnMedia];
|
|
274
|
+
const media = dedupeBncrTurnMedia([...contextMedia, ...currentTurnMedia]);
|
|
211
275
|
|
|
212
276
|
return Promise.resolve(
|
|
213
277
|
resolveBncrChannelInboundRuntime(api).buildContext({
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '../core/outbox-queue.ts';
|
|
8
8
|
import { formatDisplayScope } from '../core/targets.ts';
|
|
9
9
|
import type { BncrRecentOutboundEntry, OutboxEntry } from '../core/types.ts';
|
|
10
|
+
import type { BncrConversationHistoryMap } from '../messaging/inbound/conversation-history.ts';
|
|
10
11
|
import {
|
|
11
12
|
type BncrOutboundReplayCache,
|
|
12
13
|
type BncrOutboundReplayEntry,
|
|
@@ -42,6 +43,8 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
42
43
|
deadLetterSinceStartByAccount: Map<string, number>;
|
|
43
44
|
lastOutboundByAccount: Map<string, number>;
|
|
44
45
|
outboundReplayCache?: BncrOutboundReplayCache;
|
|
46
|
+
conversationHistories?: BncrConversationHistoryMap;
|
|
47
|
+
resolveOutboundHistoryLimit?: (entry: OutboxEntry) => number;
|
|
45
48
|
resolveOutboundSender?: (entry: OutboxEntry) => { sender: string; senderId?: string };
|
|
46
49
|
isOutboundAckRequired?: (accountId: string) => boolean;
|
|
47
50
|
scheduleSave: () => void;
|
|
@@ -64,6 +67,12 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
64
67
|
};
|
|
65
68
|
recordBncrOutboundReplay({
|
|
66
69
|
cache: runtime.outboundReplayCache,
|
|
70
|
+
...(runtime.conversationHistories
|
|
71
|
+
? { conversationHistories: runtime.conversationHistories }
|
|
72
|
+
: {}),
|
|
73
|
+
...(runtime.resolveOutboundHistoryLimit
|
|
74
|
+
? { historyLimit: runtime.resolveOutboundHistoryLimit(entry) }
|
|
75
|
+
: {}),
|
|
67
76
|
entry,
|
|
68
77
|
sender: senderInfo.sender,
|
|
69
78
|
senderId: senderInfo.senderId,
|