@xmoxmo/bncr 0.6.2 → 0.6.4
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 +3 -0
- package/scripts/selfcheck.mjs +3 -0
- package/src/channel.ts +134 -7
- package/src/messaging/inbound/commands.ts +62 -26
- package/src/messaging/inbound/conversation-history-serial.ts +258 -0
- package/src/messaging/inbound/conversation-history.ts +174 -60
- package/src/messaging/inbound/dispatch.ts +557 -137
- 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 +128 -19
- package/src/messaging/inbound/reply-dispatch.ts +7 -6
- package/src/messaging/inbound/scene-admin.ts +17 -12
- package/src/messaging/inbound/turn-context.ts +2 -9
- package/src/plugin/bridge-lifecycle.ts +8 -0
- package/src/plugin/bridge-outbox-facade.ts +6 -1
- package/src/plugin/channel-runtime-constants.ts +2 -0
- package/src/plugin/outbox-drain-post-push.ts +3 -0
- package/src/plugin/sqlite-state.ts +2109 -0
- package/src/plugin/state-store.ts +704 -46
- package/src/plugin/state-transient-runtime-group.ts +14 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { DEFAULT_GROUP_HISTORY_LIMIT as DEFAULT_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,
|
|
@@ -14,11 +17,18 @@ import {
|
|
|
14
17
|
type BncrConversationHistoryMap,
|
|
15
18
|
type BncrHistoryEntry,
|
|
16
19
|
buildBncrConversationHistoryKey,
|
|
17
|
-
clearBncrPendingConversationHistory,
|
|
18
20
|
readBncrPendingConversationHistorySnapshot,
|
|
21
|
+
readConversationHistoryVersion,
|
|
19
22
|
recordBncrPendingConversationMedia,
|
|
20
23
|
recordBncrPendingConversationText,
|
|
24
|
+
removeBncrConversationHistoryMessageIds,
|
|
25
|
+
resolveBncrConversationHistoryMessageId,
|
|
26
|
+
resolveBncrHistoryLimit,
|
|
21
27
|
} from './conversation-history.ts';
|
|
28
|
+
import {
|
|
29
|
+
type ConversationHistorySerialHandle,
|
|
30
|
+
runConversationHistorySerial,
|
|
31
|
+
} from './conversation-history-serial.ts';
|
|
22
32
|
import {
|
|
23
33
|
assertInboundMediaBase64Size,
|
|
24
34
|
decodeInboundMediaBase64,
|
|
@@ -27,11 +37,16 @@ import {
|
|
|
27
37
|
prepareBncrInboundDispatch,
|
|
28
38
|
resolveBncrInboundConversation,
|
|
29
39
|
} from './dispatch-prep.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,
|
|
45
|
+
buildBncrOutboundReplayKey,
|
|
33
46
|
readBncrOutboundReplaySnapshot,
|
|
47
|
+
removeBncrOutboundReplayMessageIds,
|
|
34
48
|
} from './outbound-replay-cache.ts';
|
|
49
|
+
import { parseBncrInboundParams } from './parse.ts';
|
|
35
50
|
import { runBncrInboundReplyDispatch } from './reply-dispatch.ts';
|
|
36
51
|
import { buildBncrInboundTurnContext } from './turn-context.ts';
|
|
37
52
|
|
|
@@ -50,10 +65,14 @@ export async function dispatchBncrInbound(params: {
|
|
|
50
65
|
canonicalAgentId: string;
|
|
51
66
|
shouldDispatch?: boolean;
|
|
52
67
|
shouldAccumulate?: boolean;
|
|
68
|
+
forceSilentHistoryFlush?: boolean;
|
|
69
|
+
forceSilentHistoryFlushVersion?: number;
|
|
70
|
+
forceSilentHistoryFlushMessageId?: string;
|
|
53
71
|
resolvedAgentId?: string;
|
|
54
72
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
55
73
|
conversationHistories: BncrConversationHistoryMap;
|
|
56
74
|
outboundReplayCache?: BncrOutboundReplayCache;
|
|
75
|
+
historyShardQueue?: BncrHistoryShardQueue;
|
|
57
76
|
defaultAdminAgentId: string;
|
|
58
77
|
defaultPublicAgentId: string;
|
|
59
78
|
now: () => number;
|
|
@@ -71,6 +90,9 @@ export async function dispatchBncrInbound(params: {
|
|
|
71
90
|
canonicalAgentId,
|
|
72
91
|
shouldDispatch: initialShouldDispatch = true,
|
|
73
92
|
shouldAccumulate = initialShouldDispatch,
|
|
93
|
+
forceSilentHistoryFlush = false,
|
|
94
|
+
forceSilentHistoryFlushVersion,
|
|
95
|
+
forceSilentHistoryFlushMessageId,
|
|
74
96
|
resolvedAgentId,
|
|
75
97
|
sceneRegistry = new Map(),
|
|
76
98
|
conversationHistories = new Map(),
|
|
@@ -84,8 +106,10 @@ export async function dispatchBncrInbound(params: {
|
|
|
84
106
|
scheduleSave,
|
|
85
107
|
logger,
|
|
86
108
|
} = params;
|
|
109
|
+
const historyShardQueue =
|
|
110
|
+
params.historyShardQueue ?? getBncrHistoryShardQueue(conversationHistories) ?? undefined;
|
|
87
111
|
const { accountId, clientId, msgId, extracted, mimeType, peer } = parsed;
|
|
88
|
-
let shouldDispatch = initialShouldDispatch;
|
|
112
|
+
let shouldDispatch = initialShouldDispatch || forceSilentHistoryFlush;
|
|
89
113
|
|
|
90
114
|
const nativeCommand = await handleBncrNativeCommand({
|
|
91
115
|
api,
|
|
@@ -143,168 +167,564 @@ export async function dispatchBncrInbound(params: {
|
|
|
143
167
|
'[bncr] inbound missing clientId for chat identity; using route identity fallback',
|
|
144
168
|
);
|
|
145
169
|
}
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Record the message first so overflow context includes it
|
|
160
|
-
recordBncrPendingConversationText({
|
|
161
|
-
historyMap: conversationHistories,
|
|
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,
|
|
162
183
|
parsed,
|
|
184
|
+
msgId,
|
|
185
|
+
peer,
|
|
186
|
+
senderIdForContext,
|
|
163
187
|
senderDisplayName,
|
|
164
|
-
|
|
165
|
-
|
|
188
|
+
ownerAllowFrom,
|
|
189
|
+
bridgeSenderId,
|
|
190
|
+
bridgeSenderName,
|
|
166
191
|
historyLimit: resolvedHistoryLimit,
|
|
192
|
+
resolution,
|
|
193
|
+
prepared,
|
|
194
|
+
conversationHistories: new Map(),
|
|
195
|
+
shouldDispatch: true,
|
|
196
|
+
silentHistoryFlush: false,
|
|
197
|
+
pendingHistoryEntries: [],
|
|
167
198
|
});
|
|
168
|
-
await
|
|
169
|
-
|
|
199
|
+
await runBncrInboundReplyDispatch({
|
|
200
|
+
api,
|
|
201
|
+
channelId,
|
|
202
|
+
cfg,
|
|
170
203
|
parsed,
|
|
204
|
+
msgId,
|
|
205
|
+
peer,
|
|
206
|
+
rawBody,
|
|
207
|
+
storePath,
|
|
208
|
+
ctxPayload,
|
|
209
|
+
resolution,
|
|
210
|
+
replyRouteFact,
|
|
211
|
+
senderIdForContext,
|
|
171
212
|
senderDisplayName,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
213
|
+
shouldDispatch: true,
|
|
214
|
+
silentHistoryFlush: false,
|
|
215
|
+
setInboundActivity,
|
|
216
|
+
scheduleSave,
|
|
217
|
+
enqueueFromReply,
|
|
177
218
|
});
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
}
|
|
186
281
|
}
|
|
187
282
|
}
|
|
188
283
|
}
|
|
189
|
-
}
|
|
190
284
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (pendingHistoryEntries.length > 0) {
|
|
198
|
-
clearBncrPendingConversationHistory({
|
|
285
|
+
if (historyKey && historyShardQueue?.reconcileHistoryMemory) {
|
|
286
|
+
await historyShardQueue.reconcileHistoryMemory(historyKey);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (shouldDispatch) {
|
|
290
|
+
pendingHistoryEntries = readBncrPendingConversationHistorySnapshot({
|
|
199
291
|
historyMap: conversationHistories,
|
|
200
292
|
parsed,
|
|
201
293
|
historyLimit: resolvedHistoryLimit,
|
|
202
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,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (shouldClearLocalCache) {
|
|
398
|
+
removeBncrOutboundReplayMessageIds({
|
|
399
|
+
cache: outboundReplayCache,
|
|
400
|
+
parsed,
|
|
401
|
+
accountId,
|
|
402
|
+
messageIds: snapshotMessageIds,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
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
|
+
});
|
|
203
420
|
}
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
421
|
+
const ctxPayload = await buildBncrInboundTurnContext({
|
|
422
|
+
api,
|
|
423
|
+
cfg,
|
|
424
|
+
channelId,
|
|
207
425
|
parsed,
|
|
208
|
-
|
|
209
|
-
|
|
426
|
+
msgId,
|
|
427
|
+
peer,
|
|
428
|
+
senderIdForContext,
|
|
429
|
+
senderDisplayName,
|
|
430
|
+
ownerAllowFrom,
|
|
431
|
+
bridgeSenderId,
|
|
432
|
+
bridgeSenderName,
|
|
433
|
+
historyLimit: resolvedHistoryLimit,
|
|
434
|
+
resolution,
|
|
435
|
+
prepared,
|
|
436
|
+
conversationHistories,
|
|
437
|
+
shouldDispatch,
|
|
438
|
+
silentHistoryFlush,
|
|
439
|
+
pendingHistoryEntries,
|
|
210
440
|
});
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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,
|
|
454
|
+
parsed,
|
|
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,
|
|
469
|
+
});
|
|
470
|
+
scheduleSave();
|
|
471
|
+
return {
|
|
472
|
+
accountId,
|
|
473
|
+
sessionKey,
|
|
474
|
+
taskKey: extracted.taskKey ?? null,
|
|
475
|
+
msgId: msgId ?? null,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
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,
|
|
215
560
|
});
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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,
|
|
241
631
|
});
|
|
242
632
|
}
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
pendingHistoryEntries = [
|
|
253
|
-
...withoutCurrent.slice(-(resolvedHistoryLimit - 1)),
|
|
254
|
-
currentEntry,
|
|
255
|
-
];
|
|
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
|
+
);
|
|
256
642
|
}
|
|
257
643
|
}
|
|
258
|
-
|
|
644
|
+
scheduleSave();
|
|
259
645
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
646
|
+
return {
|
|
647
|
+
accountId,
|
|
648
|
+
sessionKey,
|
|
649
|
+
taskKey: extracted.taskKey ?? null,
|
|
650
|
+
msgId: msgId ?? null,
|
|
651
|
+
};
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
return runConversationHistorySerial(historyKey || '', processTurn, {
|
|
655
|
+
to: resolution.canonicalTo,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
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',
|
|
279
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;
|
|
280
700
|
|
|
281
|
-
await
|
|
701
|
+
await dispatchBncrInbound({
|
|
282
702
|
api,
|
|
283
703
|
channelId,
|
|
284
704
|
cfg,
|
|
285
705
|
parsed,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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,
|
|
300
729
|
});
|
|
301
|
-
|
|
302
|
-
scheduleSave();
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
accountId,
|
|
306
|
-
sessionKey,
|
|
307
|
-
taskKey: extracted.taskKey ?? null,
|
|
308
|
-
msgId: msgId ?? null,
|
|
309
|
-
};
|
|
310
730
|
}
|