@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
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { DEFAULT_GROUP_HISTORY_LIMIT } from 'openclaw/plugin-sdk/reply-history';
|
|
2
1
|
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
2
|
+
import { parseStrictBncrSessionKey } from '../../core/targets.ts';
|
|
3
|
+
import type { OutboxEntry } from '../../core/types.ts';
|
|
3
4
|
import type { BncrSceneRecord } from '../../plugin/channel-runtime-types.ts';
|
|
4
5
|
import { buildSceneKey } from '../../plugin/scene-registry.ts';
|
|
6
|
+
import type { BncrHistoryShardQueue } from '../../plugin/sqlite-state.ts';
|
|
7
|
+
import { getBncrHistoryShardQueue } from '../../plugin/state-store.ts';
|
|
5
8
|
import { handleBncrNativeCommand } from './commands.ts';
|
|
6
9
|
import type {
|
|
7
10
|
BncrEnqueueFromReply,
|
|
@@ -10,6 +13,22 @@ import type {
|
|
|
10
13
|
BncrInboundLogger,
|
|
11
14
|
BncrRememberSessionRoute,
|
|
12
15
|
} from './contracts.ts';
|
|
16
|
+
import {
|
|
17
|
+
type BncrConversationHistoryMap,
|
|
18
|
+
type BncrHistoryEntry,
|
|
19
|
+
buildBncrConversationHistoryKey,
|
|
20
|
+
readBncrPendingConversationHistorySnapshot,
|
|
21
|
+
readConversationHistoryVersion,
|
|
22
|
+
recordBncrPendingConversationMedia,
|
|
23
|
+
recordBncrPendingConversationText,
|
|
24
|
+
removeBncrConversationHistoryMessageIds,
|
|
25
|
+
resolveBncrConversationHistoryMessageId,
|
|
26
|
+
resolveBncrHistoryLimit,
|
|
27
|
+
} from './conversation-history.ts';
|
|
28
|
+
import {
|
|
29
|
+
type ConversationHistorySerialHandle,
|
|
30
|
+
runConversationHistorySerial,
|
|
31
|
+
} from './conversation-history-serial.ts';
|
|
13
32
|
import {
|
|
14
33
|
assertInboundMediaBase64Size,
|
|
15
34
|
decodeInboundMediaBase64,
|
|
@@ -18,21 +37,16 @@ import {
|
|
|
18
37
|
prepareBncrInboundDispatch,
|
|
19
38
|
resolveBncrInboundConversation,
|
|
20
39
|
} from './dispatch-prep.ts';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
type BncrHistoryEntry,
|
|
24
|
-
buildBncrGroupHistoryKey,
|
|
25
|
-
clearBncrPendingGroupHistory,
|
|
26
|
-
readBncrPendingGroupHistorySnapshot,
|
|
27
|
-
recordBncrPendingGroupMedia,
|
|
28
|
-
recordBncrPendingGroupText,
|
|
29
|
-
} from './group-history.ts';
|
|
40
|
+
import { runBncrHistoryShardWithLeaseRenewal } from './history-shard-worker.ts';
|
|
41
|
+
import { isBncrStopCommandText } from './native-command.ts';
|
|
30
42
|
import {
|
|
31
43
|
type BncrOutboundReplayCache,
|
|
32
44
|
type BncrOutboundReplayEntry,
|
|
33
|
-
|
|
45
|
+
buildBncrOutboundReplayKey,
|
|
34
46
|
readBncrOutboundReplaySnapshot,
|
|
47
|
+
removeBncrOutboundReplayMessageIds,
|
|
35
48
|
} from './outbound-replay-cache.ts';
|
|
49
|
+
import { parseBncrInboundParams } from './parse.ts';
|
|
36
50
|
import { runBncrInboundReplyDispatch } from './reply-dispatch.ts';
|
|
37
51
|
import { buildBncrInboundTurnContext } from './turn-context.ts';
|
|
38
52
|
|
|
@@ -51,10 +65,14 @@ export async function dispatchBncrInbound(params: {
|
|
|
51
65
|
canonicalAgentId: string;
|
|
52
66
|
shouldDispatch?: boolean;
|
|
53
67
|
shouldAccumulate?: boolean;
|
|
68
|
+
forceSilentHistoryFlush?: boolean;
|
|
69
|
+
forceSilentHistoryFlushVersion?: number;
|
|
70
|
+
forceSilentHistoryFlushMessageId?: string;
|
|
54
71
|
resolvedAgentId?: string;
|
|
55
72
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
56
|
-
|
|
73
|
+
conversationHistories: BncrConversationHistoryMap;
|
|
57
74
|
outboundReplayCache?: BncrOutboundReplayCache;
|
|
75
|
+
historyShardQueue?: BncrHistoryShardQueue;
|
|
58
76
|
defaultAdminAgentId: string;
|
|
59
77
|
defaultPublicAgentId: string;
|
|
60
78
|
now: () => number;
|
|
@@ -72,9 +90,12 @@ export async function dispatchBncrInbound(params: {
|
|
|
72
90
|
canonicalAgentId,
|
|
73
91
|
shouldDispatch: initialShouldDispatch = true,
|
|
74
92
|
shouldAccumulate = initialShouldDispatch,
|
|
93
|
+
forceSilentHistoryFlush = false,
|
|
94
|
+
forceSilentHistoryFlushVersion,
|
|
95
|
+
forceSilentHistoryFlushMessageId,
|
|
75
96
|
resolvedAgentId,
|
|
76
97
|
sceneRegistry = new Map(),
|
|
77
|
-
|
|
98
|
+
conversationHistories = new Map(),
|
|
78
99
|
outboundReplayCache = new Map(),
|
|
79
100
|
defaultAdminAgentId,
|
|
80
101
|
defaultPublicAgentId,
|
|
@@ -85,8 +106,10 @@ export async function dispatchBncrInbound(params: {
|
|
|
85
106
|
scheduleSave,
|
|
86
107
|
logger,
|
|
87
108
|
} = params;
|
|
109
|
+
const historyShardQueue =
|
|
110
|
+
params.historyShardQueue ?? getBncrHistoryShardQueue(conversationHistories) ?? undefined;
|
|
88
111
|
const { accountId, clientId, msgId, extracted, mimeType, peer } = parsed;
|
|
89
|
-
let shouldDispatch = initialShouldDispatch;
|
|
112
|
+
let shouldDispatch = initialShouldDispatch || forceSilentHistoryFlush;
|
|
90
113
|
|
|
91
114
|
const nativeCommand = await handleBncrNativeCommand({
|
|
92
115
|
api,
|
|
@@ -144,142 +167,564 @@ export async function dispatchBncrInbound(params: {
|
|
|
144
167
|
'[bncr] inbound missing clientId for chat identity; using route identity fallback',
|
|
145
168
|
);
|
|
146
169
|
}
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (!shouldDispatch && shouldAccumulate) {
|
|
161
|
-
const historyKey = buildBncrGroupHistoryKey(parsed);
|
|
162
|
-
// Record the message first so overflow context includes it
|
|
163
|
-
recordBncrPendingGroupText({
|
|
164
|
-
historyMap: groupHistories,
|
|
170
|
+
const historyKey = buildBncrConversationHistoryKey(parsed);
|
|
171
|
+
const resolvedHistoryLimit = resolveBncrHistoryLimit(
|
|
172
|
+
sceneRegistry.get(buildSceneKey(parsed))?.historyLimit,
|
|
173
|
+
);
|
|
174
|
+
const isStopCommand = isBncrStopCommandText(rawBody);
|
|
175
|
+
if (isStopCommand && shouldDispatch) {
|
|
176
|
+
// A stop command must not wait behind the history serial chain or mutate
|
|
177
|
+
// the accumulated conversation window. Deliver it immediately so the host
|
|
178
|
+
// can abort the active run instead of queuing behind it.
|
|
179
|
+
const ctxPayload = await buildBncrInboundTurnContext({
|
|
180
|
+
api,
|
|
181
|
+
cfg,
|
|
182
|
+
channelId,
|
|
165
183
|
parsed,
|
|
184
|
+
msgId,
|
|
185
|
+
peer,
|
|
186
|
+
senderIdForContext,
|
|
166
187
|
senderDisplayName,
|
|
167
|
-
|
|
168
|
-
|
|
188
|
+
ownerAllowFrom,
|
|
189
|
+
bridgeSenderId,
|
|
190
|
+
bridgeSenderName,
|
|
169
191
|
historyLimit: resolvedHistoryLimit,
|
|
192
|
+
resolution,
|
|
193
|
+
prepared,
|
|
194
|
+
conversationHistories: new Map(),
|
|
195
|
+
shouldDispatch: true,
|
|
196
|
+
silentHistoryFlush: false,
|
|
197
|
+
pendingHistoryEntries: [],
|
|
170
198
|
});
|
|
171
|
-
await
|
|
172
|
-
|
|
199
|
+
await runBncrInboundReplyDispatch({
|
|
200
|
+
api,
|
|
201
|
+
channelId,
|
|
202
|
+
cfg,
|
|
173
203
|
parsed,
|
|
204
|
+
msgId,
|
|
205
|
+
peer,
|
|
206
|
+
rawBody,
|
|
207
|
+
storePath,
|
|
208
|
+
ctxPayload,
|
|
209
|
+
resolution,
|
|
210
|
+
replyRouteFact,
|
|
211
|
+
senderIdForContext,
|
|
174
212
|
senderDisplayName,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
213
|
+
shouldDispatch: true,
|
|
214
|
+
silentHistoryFlush: false,
|
|
215
|
+
setInboundActivity,
|
|
216
|
+
scheduleSave,
|
|
217
|
+
enqueueFromReply,
|
|
180
218
|
});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
219
|
+
scheduleSave();
|
|
220
|
+
return {
|
|
221
|
+
accountId,
|
|
222
|
+
sessionKey,
|
|
223
|
+
taskKey: extracted.taskKey ?? null,
|
|
224
|
+
msgId: msgId ?? null,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const processTurn = async (handle: ConversationHistorySerialHandle) => {
|
|
228
|
+
handle.phase('snapshot');
|
|
229
|
+
const serialOwner = handle.owner();
|
|
230
|
+
let silentHistoryFlush = forceSilentHistoryFlush;
|
|
231
|
+
let pendingHistoryEntries: BncrHistoryEntry[] = [];
|
|
232
|
+
let snapshotMessageIds: string[] = [];
|
|
233
|
+
let cacheKey: string | undefined;
|
|
234
|
+
let historyShardId: number | null = null;
|
|
235
|
+
let historyShardDeliveryId: string | undefined;
|
|
236
|
+
let uploadSettled = false;
|
|
237
|
+
if (historyKey && forceSilentHistoryFlushVersion !== undefined) {
|
|
238
|
+
const currentHistoryVersion = readConversationHistoryVersion(
|
|
239
|
+
conversationHistories,
|
|
240
|
+
historyKey,
|
|
241
|
+
);
|
|
242
|
+
if (currentHistoryVersion !== forceSilentHistoryFlushVersion) {
|
|
243
|
+
scheduleSave();
|
|
244
|
+
return {
|
|
245
|
+
accountId,
|
|
246
|
+
sessionKey,
|
|
247
|
+
taskKey: extracted.taskKey ?? null,
|
|
248
|
+
msgId: msgId ?? null,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (shouldAccumulate) {
|
|
253
|
+
// Record the message first so overflow context includes it
|
|
254
|
+
recordBncrPendingConversationText({
|
|
255
|
+
historyMap: conversationHistories,
|
|
256
|
+
parsed,
|
|
257
|
+
senderDisplayName,
|
|
258
|
+
senderId: senderIdForContext,
|
|
259
|
+
bodyText: rawBody,
|
|
260
|
+
historyLimit: resolvedHistoryLimit,
|
|
261
|
+
});
|
|
262
|
+
recordBncrPendingConversationMedia({
|
|
263
|
+
historyMap: conversationHistories,
|
|
264
|
+
parsed,
|
|
265
|
+
senderDisplayName,
|
|
266
|
+
senderId: senderIdForContext,
|
|
267
|
+
bodyText: rawBody,
|
|
268
|
+
mediaItems,
|
|
269
|
+
mediaContentType: mediaContentType || mimeType,
|
|
270
|
+
historyLimit: resolvedHistoryLimit,
|
|
271
|
+
});
|
|
272
|
+
if (historyKey) {
|
|
273
|
+
const entries = conversationHistories.get(historyKey);
|
|
274
|
+
if (entries && resolvedHistoryLimit > 0 && entries.length >= resolvedHistoryLimit) {
|
|
275
|
+
const sceneForHistory = sceneRegistry.get(buildSceneKey(parsed));
|
|
276
|
+
const historyForce = sceneForHistory?.historyForce !== false;
|
|
277
|
+
if (historyForce) {
|
|
278
|
+
silentHistoryFlush = true;
|
|
279
|
+
shouldDispatch = true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (historyKey && historyShardQueue?.reconcileHistoryMemory) {
|
|
286
|
+
await historyShardQueue.reconcileHistoryMemory(historyKey);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (shouldDispatch) {
|
|
290
|
+
pendingHistoryEntries = readBncrPendingConversationHistorySnapshot({
|
|
291
|
+
historyMap: conversationHistories,
|
|
292
|
+
parsed,
|
|
293
|
+
historyLimit: resolvedHistoryLimit,
|
|
294
|
+
});
|
|
295
|
+
const legacyOutboundEntries = readBncrOutboundReplaySnapshot({
|
|
296
|
+
cache: outboundReplayCache,
|
|
297
|
+
parsed,
|
|
298
|
+
accountId,
|
|
299
|
+
excludeMessageId: msgId,
|
|
300
|
+
});
|
|
301
|
+
if (
|
|
302
|
+
forceSilentHistoryFlush &&
|
|
303
|
+
pendingHistoryEntries.length === 0 &&
|
|
304
|
+
legacyOutboundEntries.length === 0
|
|
305
|
+
) {
|
|
306
|
+
scheduleSave();
|
|
307
|
+
return {
|
|
308
|
+
accountId,
|
|
309
|
+
sessionKey,
|
|
310
|
+
taskKey: extracted.taskKey ?? null,
|
|
311
|
+
msgId: msgId ?? null,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const knownMessageIds = new Set(
|
|
315
|
+
pendingHistoryEntries
|
|
316
|
+
.map((entry) => entry.messageId)
|
|
317
|
+
.filter((messageId): messageId is string => Boolean(messageId)),
|
|
318
|
+
);
|
|
319
|
+
for (const entry of legacyOutboundEntries) {
|
|
320
|
+
if (entry.messageId && knownMessageIds.has(entry.messageId)) continue;
|
|
321
|
+
if (entry.messageId) knownMessageIds.add(entry.messageId);
|
|
322
|
+
pendingHistoryEntries.push({
|
|
323
|
+
sender: entry.sender,
|
|
324
|
+
...(entry.senderId ? { senderId: entry.senderId } : {}),
|
|
325
|
+
body: entry.body,
|
|
326
|
+
...(typeof entry.timestamp === 'number' ? { timestamp: entry.timestamp } : {}),
|
|
327
|
+
...(entry.messageId ? { messageId: entry.messageId } : {}),
|
|
328
|
+
role: 'assistant',
|
|
329
|
+
...(Array.isArray(entry.media)
|
|
330
|
+
? {
|
|
331
|
+
media: entry.media.map((item) => ({
|
|
332
|
+
path: item.path,
|
|
333
|
+
contentType: item.contentType,
|
|
334
|
+
kind: item.kind,
|
|
335
|
+
messageId: item.messageId,
|
|
336
|
+
})),
|
|
337
|
+
}
|
|
338
|
+
: {}),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
pendingHistoryEntries.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
|
|
342
|
+
if (pendingHistoryEntries.length > resolvedHistoryLimit) {
|
|
343
|
+
let currentMessageId = String(
|
|
344
|
+
(forceSilentHistoryFlush && forceSilentHistoryFlushMessageId) || '',
|
|
345
|
+
).trim();
|
|
346
|
+
if (!currentMessageId) {
|
|
347
|
+
// A platform may omit message ids. The latest recorded in-memory entry
|
|
348
|
+
// is the current message and must survive the window trim even when
|
|
349
|
+
// other entries carry later timestamps.
|
|
350
|
+
currentMessageId = String(
|
|
351
|
+
resolveBncrConversationHistoryMessageId(parsed) ||
|
|
352
|
+
(shouldAccumulate
|
|
353
|
+
? historyKey
|
|
354
|
+
? conversationHistories.get(historyKey)?.at(-1)?.messageId
|
|
355
|
+
: undefined
|
|
356
|
+
: undefined) ||
|
|
357
|
+
'',
|
|
358
|
+
).trim();
|
|
359
|
+
}
|
|
360
|
+
const currentEntry = currentMessageId
|
|
361
|
+
? pendingHistoryEntries.find((entry) => entry.messageId === currentMessageId)
|
|
362
|
+
: undefined;
|
|
363
|
+
pendingHistoryEntries = pendingHistoryEntries.slice(-resolvedHistoryLimit);
|
|
364
|
+
if (currentEntry && !pendingHistoryEntries.includes(currentEntry)) {
|
|
365
|
+
const withoutCurrent = pendingHistoryEntries.filter((entry) => entry !== currentEntry);
|
|
366
|
+
pendingHistoryEntries = [
|
|
367
|
+
...withoutCurrent.slice(-(resolvedHistoryLimit - 1)),
|
|
368
|
+
currentEntry,
|
|
369
|
+
];
|
|
370
|
+
pendingHistoryEntries.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
cacheKey = buildBncrOutboundReplayKey(parsed, accountId) ?? undefined;
|
|
374
|
+
const historyEntriesBeforeUpload: readonly BncrHistoryEntry[] = historyKey
|
|
375
|
+
? conversationHistories.get(historyKey) || []
|
|
376
|
+
: [];
|
|
377
|
+
const cacheEntriesBeforeUpload: readonly BncrOutboundReplayEntry[] = cacheKey
|
|
378
|
+
? outboundReplayCache.get(cacheKey) || []
|
|
379
|
+
: [];
|
|
380
|
+
snapshotMessageIds = Array.from(
|
|
381
|
+
new Set(
|
|
382
|
+
[
|
|
383
|
+
...historyEntriesBeforeUpload.map((entry) => entry.messageId),
|
|
384
|
+
...cacheEntriesBeforeUpload.map((entry) => entry.messageId),
|
|
385
|
+
].filter((messageId): messageId is string => Boolean(messageId)),
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
handle.setCleanup(() => {
|
|
389
|
+
const shouldClearLocalCache = historyShardId === null || uploadSettled;
|
|
390
|
+
if (historyKey && shouldClearLocalCache) {
|
|
391
|
+
removeBncrConversationHistoryMessageIds({
|
|
392
|
+
historyMap: conversationHistories,
|
|
393
|
+
historyKey,
|
|
394
|
+
messageIds: snapshotMessageIds,
|
|
191
395
|
});
|
|
192
|
-
|
|
193
|
-
|
|
396
|
+
}
|
|
397
|
+
if (shouldClearLocalCache) {
|
|
398
|
+
removeBncrOutboundReplayMessageIds({
|
|
399
|
+
cache: outboundReplayCache,
|
|
194
400
|
parsed,
|
|
195
|
-
|
|
401
|
+
accountId,
|
|
402
|
+
messageIds: snapshotMessageIds,
|
|
196
403
|
});
|
|
197
|
-
shouldDispatch = true;
|
|
198
|
-
silentHistoryFlush = true;
|
|
199
404
|
}
|
|
200
|
-
|
|
405
|
+
if (uploadSettled && historyShardId !== null && historyShardQueue) {
|
|
406
|
+
try {
|
|
407
|
+
historyShardQueue.completeHistoryShard(historyShardId, serialOwner);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
emitBncrLogLine(
|
|
410
|
+
'warn',
|
|
411
|
+
`[bncr] history shard cleanup failed|key=${historyKey}|shard=${historyShardId}|reason=${String(error)}`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
handle.phase('snapshot', {
|
|
417
|
+
snapshotMessageIds,
|
|
418
|
+
cacheKey,
|
|
419
|
+
});
|
|
201
420
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
historyMap: groupHistories,
|
|
421
|
+
const ctxPayload = await buildBncrInboundTurnContext({
|
|
422
|
+
api,
|
|
423
|
+
cfg,
|
|
424
|
+
channelId,
|
|
207
425
|
parsed,
|
|
426
|
+
msgId,
|
|
427
|
+
peer,
|
|
428
|
+
senderIdForContext,
|
|
429
|
+
senderDisplayName,
|
|
430
|
+
ownerAllowFrom,
|
|
431
|
+
bridgeSenderId,
|
|
432
|
+
bridgeSenderName,
|
|
208
433
|
historyLimit: resolvedHistoryLimit,
|
|
434
|
+
resolution,
|
|
435
|
+
prepared,
|
|
436
|
+
conversationHistories,
|
|
437
|
+
shouldDispatch,
|
|
438
|
+
silentHistoryFlush,
|
|
439
|
+
pendingHistoryEntries,
|
|
209
440
|
});
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
441
|
+
if (handle.isAbandoned()) {
|
|
442
|
+
return {
|
|
443
|
+
accountId,
|
|
444
|
+
sessionKey,
|
|
445
|
+
taskKey: extracted.taskKey ?? null,
|
|
446
|
+
msgId: msgId ?? null,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (!shouldDispatch) {
|
|
450
|
+
await runBncrInboundReplyDispatch({
|
|
451
|
+
api,
|
|
452
|
+
channelId,
|
|
453
|
+
cfg,
|
|
213
454
|
parsed,
|
|
214
|
-
|
|
455
|
+
msgId,
|
|
456
|
+
peer,
|
|
457
|
+
rawBody,
|
|
458
|
+
storePath,
|
|
459
|
+
ctxPayload,
|
|
460
|
+
resolution,
|
|
461
|
+
replyRouteFact,
|
|
462
|
+
senderIdForContext,
|
|
463
|
+
senderDisplayName,
|
|
464
|
+
shouldDispatch,
|
|
465
|
+
silentHistoryFlush,
|
|
466
|
+
setInboundActivity,
|
|
467
|
+
scheduleSave,
|
|
468
|
+
enqueueFromReply,
|
|
215
469
|
});
|
|
470
|
+
scheduleSave();
|
|
471
|
+
return {
|
|
472
|
+
accountId,
|
|
473
|
+
sessionKey,
|
|
474
|
+
taskKey: extracted.taskKey ?? null,
|
|
475
|
+
msgId: msgId ?? null,
|
|
476
|
+
};
|
|
216
477
|
}
|
|
217
|
-
}
|
|
218
478
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
479
|
+
if (historyKey && historyShardQueue) {
|
|
480
|
+
let createdShardId: number | null = null;
|
|
481
|
+
try {
|
|
482
|
+
const shard = historyShardQueue.createHistoryShard({
|
|
483
|
+
historyKey,
|
|
484
|
+
accountId,
|
|
485
|
+
payloadJson: JSON.stringify({
|
|
486
|
+
version: 2,
|
|
487
|
+
historyKey,
|
|
488
|
+
accountId,
|
|
489
|
+
messageIds: snapshotMessageIds,
|
|
490
|
+
bufferKeys: cacheKey ? [cacheKey] : [],
|
|
491
|
+
dispatch: {
|
|
492
|
+
parsed,
|
|
493
|
+
msgId,
|
|
494
|
+
peer,
|
|
495
|
+
rawBody,
|
|
496
|
+
storePath,
|
|
497
|
+
ctxPayload,
|
|
498
|
+
resolution,
|
|
499
|
+
replyRouteFact,
|
|
500
|
+
senderIdForContext,
|
|
501
|
+
senderDisplayName,
|
|
502
|
+
shouldDispatch,
|
|
503
|
+
silentHistoryFlush,
|
|
504
|
+
},
|
|
505
|
+
}),
|
|
506
|
+
messageIds: snapshotMessageIds,
|
|
507
|
+
bufferKeys: cacheKey ? [cacheKey] : [],
|
|
508
|
+
});
|
|
509
|
+
if (shard !== null) {
|
|
510
|
+
createdShardId = shard.shardId;
|
|
511
|
+
// An existing shard already owns the shared snapshot. Keep stable
|
|
512
|
+
// delivery identity so this direct attempt and the worker retry
|
|
513
|
+
// carry the same ingest id instead of producing duplicates.
|
|
514
|
+
historyShardDeliveryId = `bncr-history-shard:${shard.shardId}`;
|
|
515
|
+
// The database shard owns the snapshot from this point. Removing it
|
|
516
|
+
// from the in-memory window immediately prevents a new trigger from
|
|
517
|
+
// double-sending the same messages while the shard is retried.
|
|
518
|
+
handle.phase('shard_created', {
|
|
519
|
+
snapshotMessageIds,
|
|
520
|
+
cacheKey,
|
|
521
|
+
});
|
|
522
|
+
removeBncrConversationHistoryMessageIds({
|
|
523
|
+
historyMap: conversationHistories,
|
|
524
|
+
historyKey,
|
|
525
|
+
messageIds: snapshotMessageIds,
|
|
526
|
+
});
|
|
527
|
+
removeBncrOutboundReplayMessageIds({
|
|
528
|
+
cache: outboundReplayCache,
|
|
529
|
+
parsed,
|
|
530
|
+
accountId,
|
|
531
|
+
messageIds: snapshotMessageIds,
|
|
532
|
+
});
|
|
533
|
+
if (historyShardQueue.markHistoryShardProcessing(shard.shardId, serialOwner) === false) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
`history shard activation lost|key=${historyKey}|shard=${shard.shardId}`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
// The direct attempt now owns the claim and must finish cleanup even
|
|
539
|
+
// when it reused an existing row; otherwise the worker will retry the
|
|
540
|
+
// same snapshot after this upload has settled.
|
|
541
|
+
historyShardId = shard.shardId;
|
|
542
|
+
}
|
|
543
|
+
} catch (error) {
|
|
544
|
+
historyShardId = null;
|
|
545
|
+
emitBncrLogLine(
|
|
546
|
+
'warn',
|
|
547
|
+
`[bncr] history shard create failed|key=${historyKey}|reason=${String(error)}`,
|
|
548
|
+
);
|
|
549
|
+
if (createdShardId !== null) {
|
|
550
|
+
// The shard row already exists and will be retried by the worker.
|
|
551
|
+
// Falling back to a direct upload here would duplicate the snapshot.
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
handle.phase('upload_start', {
|
|
558
|
+
snapshotMessageIds,
|
|
559
|
+
cacheKey,
|
|
226
560
|
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
561
|
+
try {
|
|
562
|
+
await runBncrHistoryShardWithLeaseRenewal({
|
|
563
|
+
shardId: historyShardId,
|
|
564
|
+
historyShardQueue,
|
|
565
|
+
owner: serialOwner,
|
|
566
|
+
task: () =>
|
|
567
|
+
runBncrInboundReplyDispatch({
|
|
568
|
+
api,
|
|
569
|
+
channelId,
|
|
570
|
+
cfg,
|
|
571
|
+
parsed,
|
|
572
|
+
msgId,
|
|
573
|
+
peer,
|
|
574
|
+
rawBody,
|
|
575
|
+
storePath,
|
|
576
|
+
ctxPayload,
|
|
577
|
+
resolution,
|
|
578
|
+
replyRouteFact,
|
|
579
|
+
senderIdForContext,
|
|
580
|
+
senderDisplayName,
|
|
581
|
+
shouldDispatch,
|
|
582
|
+
silentHistoryFlush,
|
|
583
|
+
deliveryId:
|
|
584
|
+
historyShardDeliveryId ??
|
|
585
|
+
(historyShardId !== null ? `bncr-history-shard:${historyShardId}` : undefined),
|
|
586
|
+
setInboundActivity,
|
|
587
|
+
scheduleSave,
|
|
588
|
+
enqueueFromReply,
|
|
589
|
+
}),
|
|
590
|
+
});
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (historyShardId !== null && historyShardQueue) {
|
|
593
|
+
try {
|
|
594
|
+
historyShardQueue.markHistoryShardFailed(historyShardId, error, serialOwner);
|
|
595
|
+
} catch (markError) {
|
|
596
|
+
emitBncrLogLine(
|
|
597
|
+
'warn',
|
|
598
|
+
`[bncr] history shard failed mark failed|key=${historyKey}|shard=${historyShardId}|reason=${String(markError)}`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
uploadSettled = true;
|
|
605
|
+
handle.phase('upload_end');
|
|
606
|
+
if (historyShardId !== null && historyShardQueue) {
|
|
607
|
+
try {
|
|
608
|
+
historyShardQueue.markHistoryShardCompleted(historyShardId, serialOwner);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
emitBncrLogLine(
|
|
611
|
+
'warn',
|
|
612
|
+
`[bncr] history shard complete mark failed|key=${historyKey}|shard=${historyShardId}|reason=${String(error)}`,
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
handle.phase('cache_delete_start');
|
|
617
|
+
const shouldClearLocalCache = historyShardId === null || uploadSettled;
|
|
618
|
+
if (historyKey && shouldClearLocalCache) {
|
|
619
|
+
removeBncrConversationHistoryMessageIds({
|
|
620
|
+
historyMap: conversationHistories,
|
|
621
|
+
historyKey,
|
|
622
|
+
messageIds: snapshotMessageIds,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
if (shouldClearLocalCache) {
|
|
626
|
+
removeBncrOutboundReplayMessageIds({
|
|
627
|
+
cache: outboundReplayCache,
|
|
628
|
+
parsed,
|
|
629
|
+
accountId,
|
|
630
|
+
messageIds: snapshotMessageIds,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
handle.phase('cache_delete_done');
|
|
634
|
+
if (uploadSettled && historyShardId !== null && historyShardQueue) {
|
|
635
|
+
try {
|
|
636
|
+
historyShardQueue.completeHistoryShard(historyShardId, serialOwner);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
emitBncrLogLine(
|
|
639
|
+
'warn',
|
|
640
|
+
`[bncr] history shard complete failed|key=${historyKey}|shard=${historyShardId}|reason=${String(error)}`,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
scheduleSave();
|
|
645
|
+
|
|
646
|
+
return {
|
|
230
647
|
accountId,
|
|
231
|
-
|
|
232
|
-
|
|
648
|
+
sessionKey,
|
|
649
|
+
taskKey: extracted.taskKey ?? null,
|
|
650
|
+
msgId: msgId ?? null,
|
|
651
|
+
};
|
|
652
|
+
};
|
|
233
653
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
cfg,
|
|
237
|
-
channelId,
|
|
238
|
-
parsed,
|
|
239
|
-
msgId,
|
|
240
|
-
peer,
|
|
241
|
-
senderIdForContext,
|
|
242
|
-
senderDisplayName,
|
|
243
|
-
ownerAllowFrom,
|
|
244
|
-
bridgeSenderId,
|
|
245
|
-
bridgeSenderName,
|
|
246
|
-
historyLimit: resolvedHistoryLimit,
|
|
247
|
-
resolution,
|
|
248
|
-
prepared,
|
|
249
|
-
groupHistories,
|
|
250
|
-
shouldDispatch,
|
|
251
|
-
silentHistoryFlush,
|
|
252
|
-
pendingHistoryEntries,
|
|
253
|
-
outboundReplayEntries,
|
|
654
|
+
return runConversationHistorySerial(historyKey || '', processTurn, {
|
|
655
|
+
to: resolution.canonicalTo,
|
|
254
656
|
});
|
|
657
|
+
}
|
|
255
658
|
|
|
256
|
-
|
|
659
|
+
export async function dispatchBncrOutboundHistoryFlush(params: {
|
|
660
|
+
api: BncrInboundApi;
|
|
661
|
+
channelId: string;
|
|
662
|
+
cfg: BncrInboundConfig;
|
|
663
|
+
entry: OutboxEntry;
|
|
664
|
+
historyVersion?: number;
|
|
665
|
+
canonicalAgentId: string;
|
|
666
|
+
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
667
|
+
conversationHistories: BncrConversationHistoryMap;
|
|
668
|
+
outboundReplayCache?: BncrOutboundReplayCache;
|
|
669
|
+
historyShardQueue?: BncrHistoryShardQueue;
|
|
670
|
+
defaultAdminAgentId: string;
|
|
671
|
+
defaultPublicAgentId: string;
|
|
672
|
+
now: () => number;
|
|
673
|
+
rememberSessionRoute: BncrRememberSessionRoute;
|
|
674
|
+
enqueueFromReply: BncrEnqueueFromReply;
|
|
675
|
+
setInboundActivity: (accountId: string, at: number) => void;
|
|
676
|
+
scheduleSave: () => void;
|
|
677
|
+
logger?: BncrInboundLogger;
|
|
678
|
+
}) {
|
|
679
|
+
const { entry, api, channelId, cfg, canonicalAgentId } = params;
|
|
680
|
+
const route = entry.route;
|
|
681
|
+
const parsed = parseBncrInboundParams({
|
|
682
|
+
accountId: entry.accountId,
|
|
683
|
+
platform: route.platform,
|
|
684
|
+
groupId: route.groupId || '0',
|
|
685
|
+
userId: route.userId || '0',
|
|
686
|
+
isGroup: (route.groupId || '0') !== '0',
|
|
687
|
+
sessionKey: entry.sessionKey,
|
|
688
|
+
type: 'text',
|
|
689
|
+
msg: 'bncr outbound conversation history flush',
|
|
690
|
+
...(entry.messageId ? { msgId: `bncr-outbound-flush:${entry.messageId}` } : {}),
|
|
691
|
+
clientId: 'bncr-history-system',
|
|
692
|
+
});
|
|
693
|
+
const historyKey = buildBncrConversationHistoryKey(parsed);
|
|
694
|
+
if (!historyKey) return;
|
|
695
|
+
const entries = params.conversationHistories.get(historyKey);
|
|
696
|
+
if (!entries || entries.length === 0) return;
|
|
697
|
+
const sceneForHistory = params.sceneRegistry.get(buildSceneKey(parsed));
|
|
698
|
+
if (sceneForHistory?.historyForce === false) return;
|
|
699
|
+
const sessionAgentId = parseStrictBncrSessionKey(entry.sessionKey)?.inputAgentId;
|
|
700
|
+
|
|
701
|
+
await dispatchBncrInbound({
|
|
257
702
|
api,
|
|
258
703
|
channelId,
|
|
259
704
|
cfg,
|
|
260
705
|
parsed,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
706
|
+
canonicalAgentId,
|
|
707
|
+
resolvedAgentId: sceneForHistory?.agentId || sessionAgentId,
|
|
708
|
+
shouldDispatch: true,
|
|
709
|
+
shouldAccumulate: false,
|
|
710
|
+
forceSilentHistoryFlush: true,
|
|
711
|
+
...(params.historyVersion !== undefined
|
|
712
|
+
? { forceSilentHistoryFlushVersion: params.historyVersion }
|
|
713
|
+
: {}),
|
|
714
|
+
...(params.entry.messageId
|
|
715
|
+
? { forceSilentHistoryFlushMessageId: String(params.entry.messageId).trim() }
|
|
716
|
+
: {}),
|
|
717
|
+
sceneRegistry: params.sceneRegistry,
|
|
718
|
+
conversationHistories: params.conversationHistories,
|
|
719
|
+
outboundReplayCache: params.outboundReplayCache,
|
|
720
|
+
historyShardQueue: params.historyShardQueue,
|
|
721
|
+
defaultAdminAgentId: params.defaultAdminAgentId,
|
|
722
|
+
defaultPublicAgentId: params.defaultPublicAgentId,
|
|
723
|
+
now: params.now,
|
|
724
|
+
rememberSessionRoute: params.rememberSessionRoute,
|
|
725
|
+
enqueueFromReply: params.enqueueFromReply,
|
|
726
|
+
setInboundActivity: params.setInboundActivity,
|
|
727
|
+
scheduleSave: params.scheduleSave,
|
|
728
|
+
logger: params.logger,
|
|
275
729
|
});
|
|
276
|
-
|
|
277
|
-
scheduleSave();
|
|
278
|
-
|
|
279
|
-
return {
|
|
280
|
-
accountId,
|
|
281
|
-
sessionKey,
|
|
282
|
-
taskKey: extracted.taskKey ?? null,
|
|
283
|
-
msgId: msgId ?? null,
|
|
284
|
-
};
|
|
285
730
|
}
|