@xmoxmo/bncr 0.6.1 → 0.6.3
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 +65 -1
- package/package.json +1 -1
- package/scripts/check-pack.mjs +4 -1
- package/scripts/selfcheck.mjs +4 -1
- package/src/channel.ts +144 -5
- package/src/messaging/inbound/commands.ts +62 -26
- package/src/messaging/inbound/context-facts.ts +88 -58
- package/src/messaging/inbound/conversation-history-serial.ts +258 -0
- package/src/messaging/inbound/conversation-history.ts +450 -0
- package/src/messaging/inbound/dispatch.ts +568 -123
- package/src/messaging/inbound/history-shard-worker.ts +180 -0
- package/src/messaging/inbound/native-command.ts +89 -33
- package/src/messaging/inbound/outbound-replay-cache.ts +202 -17
- package/src/messaging/inbound/reply-dispatch.ts +7 -6
- package/src/messaging/inbound/scene-admin.ts +40 -20
- package/src/messaging/inbound/turn-context.ts +101 -37
- package/src/plugin/bridge-lifecycle.ts +8 -0
- package/src/plugin/bridge-outbox-facade.ts +15 -1
- package/src/plugin/channel-components.ts +5 -5
- package/src/plugin/channel-runtime-builders-delivery.ts +2 -2
- package/src/plugin/channel-runtime-constants.ts +2 -0
- 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/outbox-drain-post-push.ts +3 -0
- package/src/plugin/scene-registry.ts +4 -4
- package/src/plugin/sqlite-state.ts +2109 -0
- package/src/plugin/state-store.ts +731 -69
- package/src/plugin/state-transient-runtime-group.ts +17 -3
- package/src/messaging/inbound/group-history.ts +0 -264
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
ParsedInbound,
|
|
19
19
|
} from './dispatch-prep.ts';
|
|
20
20
|
import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
|
|
21
|
-
import { parseBncrNativeCommand } from './native-command.ts';
|
|
21
|
+
import { isBncrStopCommandText, parseBncrNativeCommand } from './native-command.ts';
|
|
22
22
|
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
23
23
|
import { runBncrReplyDispatchSerial } from './reply-dispatch-serial.ts';
|
|
24
24
|
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
@@ -126,6 +126,7 @@ export async function runBncrInboundReplyDispatch(args: {
|
|
|
126
126
|
senderDisplayName: string;
|
|
127
127
|
shouldDispatch: boolean;
|
|
128
128
|
silentHistoryFlush?: boolean;
|
|
129
|
+
deliveryId?: string;
|
|
129
130
|
setInboundActivity: (accountId: string, at: number) => void;
|
|
130
131
|
scheduleSave: () => void;
|
|
131
132
|
enqueueFromReply: BncrEnqueueFromReply;
|
|
@@ -145,6 +146,7 @@ export async function runBncrInboundReplyDispatch(args: {
|
|
|
145
146
|
senderIdForContext,
|
|
146
147
|
shouldDispatch,
|
|
147
148
|
silentHistoryFlush = false,
|
|
149
|
+
deliveryId,
|
|
148
150
|
setInboundActivity,
|
|
149
151
|
scheduleSave,
|
|
150
152
|
enqueueFromReply,
|
|
@@ -210,10 +212,9 @@ export async function runBncrInboundReplyDispatch(args: {
|
|
|
210
212
|
return;
|
|
211
213
|
}
|
|
212
214
|
|
|
213
|
-
// Stop commands
|
|
214
|
-
|
|
215
|
-
const isStopCommand =
|
|
216
|
-
rawTrimmed === '/stop' || rawTrimmed.startsWith('/stop ') || rawTrimmed.startsWith('/stop@');
|
|
215
|
+
// Stop commands also bypass the reply serial chain; the inbound history serial
|
|
216
|
+
// bypass is handled before this function runs so they can interrupt an agent.
|
|
217
|
+
const isStopCommand = isBncrStopCommandText(rawBody);
|
|
217
218
|
const dispatchSessionKey = resolution.dispatchSessionKey;
|
|
218
219
|
|
|
219
220
|
const runStopOrSerial = () => {
|
|
@@ -225,7 +226,7 @@ export async function runBncrInboundReplyDispatch(args: {
|
|
|
225
226
|
raw: parsed,
|
|
226
227
|
adapter: {
|
|
227
228
|
ingest: () => ({
|
|
228
|
-
id: msgId
|
|
229
|
+
id: deliveryId || msgId || `${resolution.canonicalTo}:${Date.now()}`,
|
|
229
230
|
timestamp: Date.now(),
|
|
230
231
|
rawText: rawBody,
|
|
231
232
|
textForAgent: ctxPayload.BodyForAgent,
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
BncrSceneRecord,
|
|
4
4
|
BncrSceneStatus,
|
|
5
5
|
} from '../../plugin/channel-runtime-types.ts';
|
|
6
|
+
import { resolveBncrHistoryLimit } from './conversation-history.ts';
|
|
6
7
|
import type { ParsedInbound } from './dispatch-prep.ts';
|
|
7
8
|
import type { NativeCommand } from './native-command.ts';
|
|
8
9
|
|
|
@@ -49,12 +50,12 @@ const MODE_HELP_TEXT = [
|
|
|
49
50
|
].join('\n');
|
|
50
51
|
|
|
51
52
|
export const HISTORY_HELP_TEXT = [
|
|
52
|
-
'📋 Bncr
|
|
53
|
+
'📋 Bncr Conversation History Configuration',
|
|
53
54
|
'',
|
|
54
55
|
'Commands:',
|
|
55
|
-
' • /bncr history-limit <number
|
|
56
|
-
' Set history limit (default: 50)',
|
|
57
|
-
' • /bncr history-force on|off [<SceneId>]',
|
|
56
|
+
' • /bncr history-limit [<number>|clear] [<SceneId>]',
|
|
57
|
+
' Set history limit (default: 50, minimum: 2)',
|
|
58
|
+
' • /bncr history-force on|off|clear [<SceneId>]',
|
|
58
59
|
' When on, overflow triggers auto context at limit (default: on)',
|
|
59
60
|
' When off, oldest messages trim silently without context',
|
|
60
61
|
].join('\n');
|
|
@@ -88,6 +89,22 @@ function resolveCurrentGroupSceneKey(parsed: ParsedInbound): string | null {
|
|
|
88
89
|
return `${platform}:${groupId}`;
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
function resolveCurrentSceneKey(parsed: ParsedInbound): string | null {
|
|
93
|
+
const platform = normalizeToken(parsed.platform);
|
|
94
|
+
if (!platform) return null;
|
|
95
|
+
if (parsed.peer.kind === 'group') {
|
|
96
|
+
const groupId = normalizeToken(parsed.groupId);
|
|
97
|
+
if (!groupId || groupId === '0') return null;
|
|
98
|
+
return `${platform}:${groupId}`;
|
|
99
|
+
}
|
|
100
|
+
if (parsed.peer.kind === 'direct') {
|
|
101
|
+
const userId = normalizeToken(parsed.userId);
|
|
102
|
+
if (!userId || userId === '0') return null;
|
|
103
|
+
return `${platform}:${userId}`;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
91
108
|
export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdminCommand {
|
|
92
109
|
const args = splitArgs(command.argsText);
|
|
93
110
|
switch (command.command) {
|
|
@@ -242,7 +259,7 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
242
259
|
return {
|
|
243
260
|
matched: true,
|
|
244
261
|
valid: false,
|
|
245
|
-
text: 'Usage: /bncr history-limit [<number
|
|
262
|
+
text: 'Usage: /bncr history-limit [<number>|clear] [<sceneKey>]',
|
|
246
263
|
};
|
|
247
264
|
case 'history-force':
|
|
248
265
|
if (args.length === 0) {
|
|
@@ -337,13 +354,11 @@ function formatSceneDetailsLine(scene: BncrSceneRecord): string {
|
|
|
337
354
|
scene.kind === 'group' ? scene.groupId || scene.sceneKey : scene.userId || scene.sceneKey;
|
|
338
355
|
const namePart = scene.kind === 'group' ? scene.groupName || '' : scene.userName || '';
|
|
339
356
|
const labelPart = namePart ? ` name=${namePart}` : '';
|
|
340
|
-
const historyLimitVal = scene.historyLimit;
|
|
357
|
+
const historyLimitVal = resolveBncrHistoryLimit(scene.historyLimit);
|
|
341
358
|
const historyForceVal = scene.historyForce;
|
|
342
|
-
const hasNonDefaultHistory =
|
|
343
|
-
scene.kind === 'group' &&
|
|
344
|
-
((typeof historyLimitVal === 'number' && historyLimitVal !== 50) || historyForceVal === false);
|
|
359
|
+
const hasNonDefaultHistory = historyLimitVal !== 50 || historyForceVal === false;
|
|
345
360
|
const historyPart = hasNonDefaultHistory
|
|
346
|
-
? ` historyLimit=${historyLimitVal
|
|
361
|
+
? ` historyLimit=${historyLimitVal} autoFlush=${historyForceVal !== false ? 'on' : 'off'}`
|
|
347
362
|
: '';
|
|
348
363
|
return ` Details: status=${scene.status} id=${idPart}${labelPart}${historyPart}`;
|
|
349
364
|
}
|
|
@@ -475,19 +490,22 @@ 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
|
}
|
|
482
497
|
const s = sceneRegistry.get(qSceneKey);
|
|
483
498
|
if (!s) return { ok: true, text: 'Default history limit is 50.' };
|
|
484
|
-
return {
|
|
499
|
+
return {
|
|
500
|
+
ok: true,
|
|
501
|
+
text: `Current ${qSceneKey} history limit is ${resolveBncrHistoryLimit(s.historyLimit)}.`,
|
|
502
|
+
};
|
|
485
503
|
}
|
|
486
504
|
|
|
487
505
|
if (command.kind === 'history-limit-set') {
|
|
488
|
-
const sSceneKey = command.sceneKey ||
|
|
506
|
+
const sSceneKey = command.sceneKey || resolveCurrentSceneKey(parsed);
|
|
489
507
|
if (!sSceneKey) {
|
|
490
|
-
return { ok: false, text: 'Current
|
|
508
|
+
return { ok: false, text: 'Current conversation shortcut requires a valid chat.' };
|
|
491
509
|
}
|
|
492
510
|
const sExisting = sceneRegistry.get(sSceneKey);
|
|
493
511
|
if (!sExisting) return { ok: false, text: `Scene not found: ${sSceneKey}` };
|
|
@@ -503,12 +521,14 @@ export function executeSceneAdminCommand(args: {
|
|
|
503
521
|
let resolvedLimit = rawLimit;
|
|
504
522
|
if (resolvedLimit >= 51) {
|
|
505
523
|
resolvedLimit = Math.min(Math.floor(resolvedLimit), 10000);
|
|
506
|
-
} else if (resolvedLimit
|
|
507
|
-
resolvedLimit = Math.floor(
|
|
524
|
+
} else if (resolvedLimit >= 2) {
|
|
525
|
+
resolvedLimit = Math.floor(resolvedLimit);
|
|
526
|
+
} else if (resolvedLimit < 0 && Math.abs(resolvedLimit) >= 2) {
|
|
527
|
+
resolvedLimit = Math.min(Math.floor(Math.abs(resolvedLimit)), 10000);
|
|
508
528
|
} else {
|
|
509
529
|
return {
|
|
510
530
|
ok: false,
|
|
511
|
-
text: `Value too small, must be >=
|
|
531
|
+
text: `Value too small, must be >= 2, or use negative number (abs >= 2) for hidden override.`,
|
|
512
532
|
};
|
|
513
533
|
}
|
|
514
534
|
sceneRegistry.set(sSceneKey, {
|
|
@@ -520,7 +540,7 @@ export function executeSceneAdminCommand(args: {
|
|
|
520
540
|
}
|
|
521
541
|
|
|
522
542
|
if (command.kind === 'history-force-get') {
|
|
523
|
-
const qSceneKey = command.sceneKey ||
|
|
543
|
+
const qSceneKey = command.sceneKey || resolveCurrentSceneKey(parsed);
|
|
524
544
|
if (!qSceneKey) {
|
|
525
545
|
return { ok: true, text: 'Default history auto flush is on.' };
|
|
526
546
|
}
|
|
@@ -534,9 +554,9 @@ export function executeSceneAdminCommand(args: {
|
|
|
534
554
|
}
|
|
535
555
|
|
|
536
556
|
if (command.kind === 'history-force-set') {
|
|
537
|
-
const sSceneKey = command.sceneKey ||
|
|
557
|
+
const sSceneKey = command.sceneKey || resolveCurrentSceneKey(parsed);
|
|
538
558
|
if (!sSceneKey) {
|
|
539
|
-
return { ok: false, text: 'Current
|
|
559
|
+
return { ok: false, text: 'Current conversation shortcut requires a valid chat.' };
|
|
540
560
|
}
|
|
541
561
|
const sExisting = sceneRegistry.get(sSceneKey);
|
|
542
562
|
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({
|
|
@@ -44,6 +44,7 @@ export function startBncrBridgeService(
|
|
|
44
44
|
options?: { debugOnly?: boolean },
|
|
45
45
|
) => void;
|
|
46
46
|
loadState: () => Promise<void>;
|
|
47
|
+
cutoverToSqlite?: () => Promise<{ backupPath: string | null; storeMode: string }>;
|
|
47
48
|
setDebugFlag: (value: boolean) => void;
|
|
48
49
|
refreshDebugFlagFromConfig: (options?: { forceLog?: boolean }) => Promise<void>;
|
|
49
50
|
buildIntegratedDiagnostics: (accountId: string) => {
|
|
@@ -75,6 +76,13 @@ export function startBncrBridgeService(
|
|
|
75
76
|
// ignore startup canonical agent initialization errors
|
|
76
77
|
}
|
|
77
78
|
await runtime.loadState();
|
|
79
|
+
if (process.env.BNCR_SQLITE_CUTOVER === '1' && runtime.cutoverToSqlite) {
|
|
80
|
+
const cutover = await runtime.cutoverToSqlite();
|
|
81
|
+
runtime.logInfo(
|
|
82
|
+
'sqlite',
|
|
83
|
+
`cutover completed backup=${cutover.backupPath || 'none'} storeMode=${cutover.storeMode}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
78
86
|
if (typeof debug === 'boolean') runtime.setDebugFlag(debug);
|
|
79
87
|
await runtime.refreshDebugFlagFromConfig({ forceLog: true });
|
|
80
88
|
const bootDiag = runtime.buildIntegratedDiagnostics(BNCR_DEFAULT_ACCOUNT_ID);
|
|
@@ -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,7 +43,11 @@ 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;
|
|
48
|
+
resolveOutboundHistoryForce?: (entry: OutboxEntry) => boolean;
|
|
45
49
|
resolveOutboundSender?: (entry: OutboxEntry) => { sender: string; senderId?: string };
|
|
50
|
+
onConversationHistoryOverflow?: (entry: OutboxEntry, historyVersion?: number) => void;
|
|
46
51
|
isOutboundAckRequired?: (accountId: string) => boolean;
|
|
47
52
|
scheduleSave: () => void;
|
|
48
53
|
flushPushQueueBestEffort: (args?: {
|
|
@@ -62,13 +67,22 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
62
67
|
sender: entry.accountId,
|
|
63
68
|
senderId: entry.accountId,
|
|
64
69
|
};
|
|
65
|
-
recordBncrOutboundReplay({
|
|
70
|
+
const replayResult = recordBncrOutboundReplay({
|
|
66
71
|
cache: runtime.outboundReplayCache,
|
|
72
|
+
...(runtime.conversationHistories
|
|
73
|
+
? { conversationHistories: runtime.conversationHistories }
|
|
74
|
+
: {}),
|
|
75
|
+
...(runtime.resolveOutboundHistoryLimit
|
|
76
|
+
? { historyLimit: runtime.resolveOutboundHistoryLimit(entry) }
|
|
77
|
+
: {}),
|
|
67
78
|
entry,
|
|
68
79
|
sender: senderInfo.sender,
|
|
69
80
|
senderId: senderInfo.senderId,
|
|
70
81
|
status,
|
|
71
82
|
});
|
|
83
|
+
if (replayResult.historyOverflow && runtime.resolveOutboundHistoryForce?.(entry) !== false) {
|
|
84
|
+
runtime.onConversationHistoryOverflow?.(entry, replayResult.historyVersion);
|
|
85
|
+
}
|
|
72
86
|
};
|
|
73
87
|
|
|
74
88
|
const markRecentOutboundAcked = (entry: OutboxEntry) => {
|
|
@@ -4,8 +4,8 @@ import { emitBncrLogLine } from '../core/logging.ts';
|
|
|
4
4
|
import { formatDisplayScope, normalizeStoredSessionKey, parseRouteLike } from '../core/targets.ts';
|
|
5
5
|
import type { BncrRoute } from '../core/types.ts';
|
|
6
6
|
import type { BncrInboundParamsInput } from '../messaging/inbound/contracts.ts';
|
|
7
|
+
import type { BncrConversationHistoryMap } from '../messaging/inbound/conversation-history.ts';
|
|
7
8
|
import { dispatchBncrInbound } from '../messaging/inbound/dispatch.ts';
|
|
8
|
-
import type { BncrGroupHistoryMap } from '../messaging/inbound/group-history.ts';
|
|
9
9
|
import type { BncrOutboundReplayCache } from '../messaging/inbound/outbound-replay-cache.ts';
|
|
10
10
|
import { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
|
|
11
11
|
import { OUTBOUND_FLUSH_REASON, OUTBOUND_FLUSH_TRIGGER } from '../messaging/outbound/reasons.ts';
|
|
@@ -211,7 +211,7 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
211
211
|
defaultAdminAgentId: Parameters<typeof createBncrInboundHandlers>[0]['defaultAdminAgentId'];
|
|
212
212
|
defaultPublicAgentId: Parameters<typeof createBncrInboundHandlers>[0]['defaultPublicAgentId'];
|
|
213
213
|
sceneRegistry: Parameters<typeof createBncrInboundHandlers>[0]['sceneRegistry'];
|
|
214
|
-
|
|
214
|
+
conversationHistories: BncrConversationHistoryMap;
|
|
215
215
|
outboundReplayCache: BncrOutboundReplayCache;
|
|
216
216
|
prepareInboundAcceptance: Parameters<
|
|
217
217
|
typeof createBncrInboundHandlers
|
|
@@ -257,7 +257,7 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
257
257
|
defaultAdminAgentId: runtime.defaultAdminAgentId,
|
|
258
258
|
defaultPublicAgentId: runtime.defaultPublicAgentId,
|
|
259
259
|
sceneRegistry: runtime.sceneRegistry,
|
|
260
|
-
|
|
260
|
+
conversationHistories: runtime.conversationHistories,
|
|
261
261
|
outboundReplayCache: runtime.outboundReplayCache,
|
|
262
262
|
prepareInboundAcceptance: runtime.prepareInboundAcceptance,
|
|
263
263
|
formatDisplayScope,
|
|
@@ -277,7 +277,7 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
277
277
|
shouldDispatch,
|
|
278
278
|
shouldAccumulate,
|
|
279
279
|
sceneRegistry,
|
|
280
|
-
|
|
280
|
+
conversationHistories,
|
|
281
281
|
outboundReplayCache,
|
|
282
282
|
defaultAdminAgentId,
|
|
283
283
|
defaultPublicAgentId,
|
|
@@ -293,7 +293,7 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
293
293
|
shouldDispatch,
|
|
294
294
|
shouldAccumulate,
|
|
295
295
|
sceneRegistry,
|
|
296
|
-
|
|
296
|
+
conversationHistories,
|
|
297
297
|
outboundReplayCache,
|
|
298
298
|
defaultAdminAgentId,
|
|
299
299
|
defaultPublicAgentId,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
|
|
2
2
|
import type { BncrRoute, OutboxEntry } from '../core/types.ts';
|
|
3
|
-
import type {
|
|
3
|
+
import type { BncrConversationHistoryMap } from '../messaging/inbound/conversation-history.ts';
|
|
4
4
|
import type { BncrOutboundReplayCache } from '../messaging/inbound/outbound-replay-cache.ts';
|
|
5
5
|
import type { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
|
|
6
6
|
import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
|
|
@@ -107,7 +107,7 @@ export function buildBncrInboundSurfaceRuntime(deps: {
|
|
|
107
107
|
}) => string;
|
|
108
108
|
defaultPublicAgentId: () => string;
|
|
109
109
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
110
|
-
|
|
110
|
+
conversationHistories: BncrConversationHistoryMap;
|
|
111
111
|
outboundReplayCache: BncrOutboundReplayCache;
|
|
112
112
|
prepareInboundAcceptance: (args: {
|
|
113
113
|
parsed: ReturnType<typeof parseBncrInboundParams>;
|
|
@@ -12,6 +12,8 @@ export const MAX_SESSION_ROUTE_ENTRIES = 1000;
|
|
|
12
12
|
export const MAX_ACCOUNT_ACTIVITY_ENTRIES = 1000;
|
|
13
13
|
export const PUSH_DRAIN_INTERVAL_MS = 500;
|
|
14
14
|
export const PUSH_DRAIN_ACCOUNT_BUDGET = 5;
|
|
15
|
+
export const HISTORY_SHARD_WORKER_INTERVAL_MS = 1_000;
|
|
16
|
+
export const HISTORY_SHARD_WORKER_BATCH_LIMIT = 5;
|
|
15
17
|
export const PUSH_DRAIN_ACCOUNT_TIME_BUDGET_MS = 2_000;
|
|
16
18
|
export const PUSH_DRAIN_EXCEPTION_RETRY_LIMIT = 3;
|
|
17
19
|
export const PUSH_DRAIN_EXCEPTION_RETRY_DELAY_MS = 1_000;
|
|
@@ -73,29 +73,30 @@ export type BncrSceneRecord = {
|
|
|
73
73
|
lastSeenAt: number;
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
export type
|
|
76
|
+
export type BncrPersistedConversationHistoryEntry = {
|
|
77
77
|
sender: string;
|
|
78
78
|
senderId?: string;
|
|
79
|
+
role?: 'user' | 'assistant' | 'system';
|
|
79
80
|
body: string;
|
|
80
81
|
timestamp?: number;
|
|
81
82
|
messageId?: string;
|
|
82
|
-
media?:
|
|
83
|
+
media?: BncrPersistedConversationHistoryMediaEntry[];
|
|
83
84
|
};
|
|
84
85
|
|
|
85
|
-
export type
|
|
86
|
+
export type BncrPersistedConversationHistoryMediaEntry = {
|
|
86
87
|
path?: string;
|
|
87
88
|
contentType?: string;
|
|
88
89
|
kind?: 'image' | 'video' | 'audio' | 'document' | 'unknown';
|
|
89
90
|
messageId?: string;
|
|
90
91
|
};
|
|
91
92
|
|
|
92
|
-
export type
|
|
93
|
+
export type BncrPersistedConversationHistoryBucket = {
|
|
93
94
|
key: string;
|
|
94
|
-
entries:
|
|
95
|
+
entries: BncrPersistedConversationHistoryEntry[];
|
|
95
96
|
};
|
|
96
97
|
|
|
97
98
|
export type BncrPersistedOutboundReplayEntry = BncrOutboundReplayEntry;
|
|
98
|
-
export type BncrPersistedOutboundReplayMediaEntry =
|
|
99
|
+
export type BncrPersistedOutboundReplayMediaEntry = BncrPersistedConversationHistoryMediaEntry;
|
|
99
100
|
export type BncrPersistedOutboundReplayBucket = {
|
|
100
101
|
key: string;
|
|
101
102
|
entries: BncrPersistedOutboundReplayEntry[];
|
|
@@ -186,7 +187,8 @@ export type PersistedState = {
|
|
|
186
187
|
deadLetter: OutboxEntry[];
|
|
187
188
|
sessionRoutes: BncrPersistedSessionRoute[];
|
|
188
189
|
sceneRegistry?: BncrSceneRecord[];
|
|
189
|
-
|
|
190
|
+
conversationHistories?: BncrPersistedConversationHistoryBucket[];
|
|
191
|
+
groupHistories?: BncrPersistedConversationHistoryBucket[];
|
|
190
192
|
outboundReplayCache?: BncrPersistedOutboundReplayBucket[];
|
|
191
193
|
lastSessionByAccount?: BncrPersistedLastSession[];
|
|
192
194
|
lastActivityByAccount?: BncrPersistedAccountTimestamp[];
|
|
@@ -66,30 +66,12 @@ function shouldDispatchForScene(args: {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function shouldAccumulateForScene(args: {
|
|
69
|
-
|
|
69
|
+
shouldDispatch: boolean;
|
|
70
70
|
admission: ReturnType<typeof decideSceneAdmission>;
|
|
71
|
-
cfg: BncrChannelConfigRoot;
|
|
72
71
|
}) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const isBncrNativeCommand =
|
|
78
|
-
parseBncrNativeCommand(parsed.extracted.text, {
|
|
79
|
-
allowBareWhoami: parsed.isAdmin !== true,
|
|
80
|
-
}) !== null;
|
|
81
|
-
const isAdminOpenClawNativeCommand =
|
|
82
|
-
parsed.isAdmin === true &&
|
|
83
|
-
!isBncrNativeCommand &&
|
|
84
|
-
hasControlCommand(parsed.extracted.text, cfg as Parameters<typeof hasControlCommand>[1]);
|
|
85
|
-
if (isAdminOpenClawNativeCommand) return true;
|
|
86
|
-
|
|
87
|
-
const mode = admission.scene.groupReplyMode || 'admin';
|
|
88
|
-
// admin and all: no accumulation needed - messages are either dispatched directly or discarded
|
|
89
|
-
if (mode === 'admin') return false;
|
|
90
|
-
if (mode === 'all') return false;
|
|
91
|
-
// mention and hybrid: non-trigger messages accumulate for future context
|
|
92
|
-
return true;
|
|
72
|
+
if (args.shouldDispatch) return true;
|
|
73
|
+
const mode = args.admission.scene.groupReplyMode || 'admin';
|
|
74
|
+
return mode === 'mention' || mode === 'hybrid';
|
|
93
75
|
}
|
|
94
76
|
|
|
95
77
|
export async function prepareBncrInboundAcceptance(args: {
|
|
@@ -206,6 +188,9 @@ export async function prepareBncrInboundAcceptance(args: {
|
|
|
206
188
|
resolveAgentRoute: args.resolveAgentRoute,
|
|
207
189
|
});
|
|
208
190
|
|
|
191
|
+
const shouldDispatch = shouldDispatchForScene({ parsed, admission, cfg });
|
|
192
|
+
const shouldAccumulate = shouldAccumulateForScene({ shouldDispatch, admission });
|
|
193
|
+
|
|
209
194
|
return {
|
|
210
195
|
ok: true as const,
|
|
211
196
|
accountId,
|
|
@@ -213,8 +198,8 @@ export async function prepareBncrInboundAcceptance(args: {
|
|
|
213
198
|
inboundText,
|
|
214
199
|
hasMedia: Boolean(mediaBase64 || mediaPathFromTransfer),
|
|
215
200
|
resolvedAgentId: admission.agentId,
|
|
216
|
-
shouldDispatch
|
|
217
|
-
shouldAccumulate
|
|
201
|
+
shouldDispatch,
|
|
202
|
+
shouldAccumulate,
|
|
218
203
|
dispatchBy: resolveDispatchBy({ parsed, admission, cfg }),
|
|
219
204
|
};
|
|
220
205
|
}
|