@xmoxmo/bncr 0.6.5 → 0.6.6

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.
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readFile, rename } from 'node:fs/promises';
3
3
  import { basename, dirname, join } from 'node:path';
4
+ import { BNCR_DEFAULT_ACCOUNT_ID, normalizeAccountId } from '../core/accounts.ts';
4
5
  import {
5
6
  dumpRegisterDriftSnapshot,
6
7
  normalizeRegisterDriftSnapshot,
@@ -12,6 +13,7 @@ import {
12
13
  buildBncrConversationHistoryKeyFromRoute,
13
14
  resetConversationHistoryVersions,
14
15
  resolveBncrHistoryLimit,
16
+ resolveBncrSceneKeyFromHistoryKey,
15
17
  } from '../messaging/inbound/conversation-history.ts';
16
18
  import {
17
19
  getConversationHistorySerialOwner,
@@ -51,6 +53,35 @@ import type {
51
53
  const GROUP_REPLY_MODES = new Set<BncrGroupReplyMode>(['admin', 'mention', 'hybrid', 'all']);
52
54
  const DEFAULT_PERSISTED_CONVERSATION_HISTORY_LIMIT = 50;
53
55
 
56
+ /**
57
+ * Migrate legacy scene-only history keys (`tgBot:10001`) to the current
58
+ * account-scoped format (`Primary:tgBot:10001`). Keys that already carry an
59
+ * accountId prefix are returned unchanged.
60
+ */
61
+ function normalizePersistedHistoryKey(key: string): string {
62
+ const raw = String(key || '').trim();
63
+ if (!raw) return raw;
64
+ const firstColon = raw.indexOf(':');
65
+ const secondColon = raw.indexOf(':', firstColon + 1);
66
+ if (firstColon <= 0 || secondColon < 0) return `${BNCR_DEFAULT_ACCOUNT_ID}:${raw}`;
67
+ return `${normalizeAccountId(raw.slice(0, firstColon))}:${raw.slice(firstColon + 1)}`;
68
+ }
69
+
70
+ function resolvePersistedHistoryAccountId(key: string): string {
71
+ const normalized = normalizePersistedHistoryKey(key);
72
+ const separator = normalized.indexOf(':');
73
+ return separator > 0
74
+ ? normalized.slice(0, separator).trim() || BNCR_DEFAULT_ACCOUNT_ID
75
+ : BNCR_DEFAULT_ACCOUNT_ID;
76
+ }
77
+
78
+ function resolvePersistedHistoryKey(key: string, accountId: string): string {
79
+ const normalized = normalizePersistedHistoryKey(key);
80
+ const separator = normalized.indexOf(':');
81
+ const sceneKey = separator > 0 ? normalized.slice(separator + 1) : normalized;
82
+ return `${normalizeAccountId(accountId)}:${sceneKey}`;
83
+ }
84
+
54
85
  type BncrPersistedStateStoreInput = {
55
86
  outbox?: unknown;
56
87
  deadLetter?: unknown;
@@ -165,7 +196,8 @@ export function createBncrStateStore(runtime: {
165
196
  }
166
197
 
167
198
  function resolvePersistedConversationHistoryLimit(key: string): number {
168
- const scene = runtime.sceneRegistry.get(key);
199
+ const sceneKey = resolveBncrSceneKeyFromHistoryKey(key);
200
+ const scene = runtime.sceneRegistry.get(sceneKey);
169
201
  const sceneLimit = resolveBncrHistoryLimit(scene?.historyLimit);
170
202
  return Math.max(DEFAULT_PERSISTED_CONVERSATION_HISTORY_LIMIT, Math.ceil(sceneLimit * 1.2));
171
203
  }
@@ -176,7 +208,11 @@ export function createBncrStateStore(runtime: {
176
208
  ): string | null {
177
209
  const route = entries.find((entry) => runtime.parseRouteLike(entry?.route))?.route;
178
210
  if (route) {
211
+ const accountId = entries.find((entry) =>
212
+ runtime.asString(entry.accountId || '').trim(),
213
+ )?.accountId;
179
214
  return buildBncrConversationHistoryKeyFromRoute({
215
+ accountId,
180
216
  platform: route.platform,
181
217
  groupId: route.groupId,
182
218
  userId: route.userId,
@@ -184,7 +220,7 @@ export function createBncrStateStore(runtime: {
184
220
  }
185
221
  const separator = bucketKey.indexOf(':');
186
222
  if (separator < 0 || separator >= bucketKey.length - 1) return null;
187
- return bucketKey.slice(separator + 1).trim() || null;
223
+ return normalizePersistedHistoryKey(bucketKey);
188
224
  }
189
225
 
190
226
  function resolveOutboundReplayBucketLimit(
@@ -193,7 +229,8 @@ export function createBncrStateStore(runtime: {
193
229
  ): number {
194
230
  const historyKey = resolveOutboundReplayBucketHistoryKey(bucketKey, entries);
195
231
  if (!historyKey) return DEFAULT_PERSISTED_CONVERSATION_HISTORY_LIMIT;
196
- return resolveBncrHistoryLimit(runtime.sceneRegistry.get(historyKey)?.historyLimit);
232
+ const sceneKey = resolveBncrSceneKeyFromHistoryKey(historyKey);
233
+ return resolveBncrHistoryLimit(runtime.sceneRegistry.get(sceneKey)?.historyLimit);
197
234
  }
198
235
 
199
236
  function normalizeOutboundReplayBucket(
@@ -282,6 +319,7 @@ export function createBncrStateStore(runtime: {
282
319
  for (const bucket of buckets) {
283
320
  const key = runtime.asString(bucket?.key || '').trim();
284
321
  if (!key) continue;
322
+ const normalizedKey = normalizePersistedHistoryKey(key);
285
323
  const entries: BncrPersistedConversationHistoryEntry[] = [];
286
324
  const rawEntries = Array.isArray(bucket?.entries)
287
325
  ? (bucket.entries as PersistedConversationHistoryEntryInput[])
@@ -315,7 +353,7 @@ export function createBncrStateStore(runtime: {
315
353
  }
316
354
 
317
355
  const rawRole = entry?.role;
318
- const assistantMessageIds = options?.assistantMessageIdsByHistoryKey?.get(key);
356
+ const assistantMessageIds = options?.assistantMessageIdsByHistoryKey?.get(normalizedKey);
319
357
  const role =
320
358
  rawRole === 'user' || rawRole === 'assistant' || rawRole === 'system'
321
359
  ? rawRole
@@ -325,7 +363,7 @@ export function createBncrStateStore(runtime: {
325
363
  if (!messageId) {
326
364
  messageId = buildMigratedHistoryMessageId(
327
365
  JSON.stringify({
328
- key,
366
+ key: normalizedKey,
329
367
  entryIndex,
330
368
  sender,
331
369
  senderId: runtime.asString(entry?.senderId || '').trim(),
@@ -365,8 +403,10 @@ export function createBncrStateStore(runtime: {
365
403
  entries.push(normalizedEntry);
366
404
  }
367
405
  if (entries.length > 0) {
368
- const limit = resolveLimit(key);
369
- const merged = options?.merge ? [...(target.get(key) || []), ...entries] : entries;
406
+ const limit = resolveLimit(normalizedKey);
407
+ const merged = options?.merge
408
+ ? [...(target.get(normalizedKey) || []), ...entries]
409
+ : entries;
370
410
  const seenMessageIds = new Set<string>();
371
411
  const deduped: BncrPersistedConversationHistoryEntry[] = [];
372
412
  for (const entry of merged) {
@@ -375,7 +415,7 @@ export function createBncrStateStore(runtime: {
375
415
  if (messageId) seenMessageIds.add(messageId);
376
416
  deduped.push(entry);
377
417
  }
378
- target.set(key, Number.isFinite(limit) ? deduped.slice(-limit) : deduped);
418
+ target.set(normalizedKey, Number.isFinite(limit) ? deduped.slice(-limit) : deduped);
379
419
  }
380
420
  }
381
421
  }
@@ -409,6 +449,7 @@ export function createBncrStateStore(runtime: {
409
449
  sender?: unknown;
410
450
  body?: unknown;
411
451
  messageId?: unknown;
452
+ accountId?: unknown;
412
453
  route?: { platform?: unknown; groupId?: unknown; userId?: unknown };
413
454
  }>) {
414
455
  const sender = runtime.asString(entry?.sender || '').trim();
@@ -417,12 +458,17 @@ export function createBncrStateStore(runtime: {
417
458
  const messageId = runtime.asString(entry?.messageId || '').trim();
418
459
  if (!messageId) continue;
419
460
  const route = entry?.route;
461
+ const entryAccountId = runtime.asString(entry?.accountId || '').trim() || undefined;
420
462
  const historyKey =
421
463
  buildBncrConversationHistoryKeyFromRoute({
464
+ accountId: entryAccountId,
422
465
  platform: typeof route?.platform === 'string' ? route.platform : undefined,
423
466
  groupId: typeof route?.groupId === 'string' ? route.groupId : undefined,
424
467
  userId: typeof route?.userId === 'string' ? route.userId : undefined,
425
- }) ?? (bucketKey.includes(':') ? bucketKey.slice(bucketKey.indexOf(':') + 1) : '');
468
+ }) ??
469
+ (entryAccountId
470
+ ? resolvePersistedHistoryKey(bucketKey, entryAccountId)
471
+ : normalizePersistedHistoryKey(bucketKey));
426
472
  if (!historyKey) continue;
427
473
  const scopedMessageIds = messageIdsByHistoryKey.get(historyKey) ?? new Set<string>();
428
474
  scopedMessageIds.add(messageId);
@@ -440,18 +486,18 @@ export function createBncrStateStore(runtime: {
440
486
  timestamp: number;
441
487
  media: BncrPersistedConversationHistoryMediaEntry[];
442
488
  route?: BncrRoute | null;
489
+ accountId?: string;
443
490
  historyMap?: Map<string, BncrPersistedConversationHistoryEntry[]>;
444
491
  }): string | undefined {
445
492
  const route = args.route;
446
493
  const historyKey = route
447
494
  ? buildBncrConversationHistoryKeyFromRoute({
495
+ accountId: args.accountId,
448
496
  platform: route.platform,
449
497
  groupId: route.groupId,
450
498
  userId: route.userId,
451
499
  })
452
- : args.bucketKey.includes(':')
453
- ? args.bucketKey.slice(args.bucketKey.indexOf(':') + 1).trim()
454
- : '';
500
+ : normalizePersistedHistoryKey(args.bucketKey);
455
501
  if (!historyKey) return undefined;
456
502
 
457
503
  const matchingHistory = args.historyMap
@@ -491,7 +537,7 @@ export function createBncrStateStore(runtime: {
491
537
  for (const bucket of buckets) {
492
538
  const key = runtime.asString(bucket?.key || '').trim();
493
539
  if (!key) continue;
494
- const entries: BncrOutboundReplayEntry[] = [];
540
+ const entriesByHistoryKey = new Map<string, BncrOutboundReplayEntry[]>();
495
541
  const rawEntries = Array.isArray(bucket?.entries)
496
542
  ? (bucket.entries as PersistedOutboundReplayEntryInput[])
497
543
  : [];
@@ -502,6 +548,9 @@ export function createBncrStateStore(runtime: {
502
548
  const timestamp = runtime.finiteNumberOr(entry?.timestamp, 0);
503
549
  const messageId = runtime.asString(entry?.messageId || '').trim();
504
550
  const accountId = runtime.asString(entry?.accountId || '').trim();
551
+ const effectiveAccountId = runtime.normalizeAccountId(
552
+ accountId || resolvePersistedHistoryAccountId(key),
553
+ );
505
554
  const sessionKey = runtime.asString(entry?.sessionKey || '').trim();
506
555
  const type = runtime.asString(entry?.type || '').trim();
507
556
  const mediaUrl = runtime.asString(entry?.mediaUrl || '').trim();
@@ -514,6 +563,14 @@ export function createBncrStateStore(runtime: {
514
563
  groupId: entry?.route?.groupId,
515
564
  userId: entry?.route?.userId,
516
565
  });
566
+ const historyKey = route
567
+ ? buildBncrConversationHistoryKeyFromRoute({
568
+ accountId: effectiveAccountId,
569
+ platform: route.platform,
570
+ groupId: route.groupId,
571
+ userId: route.userId,
572
+ }) || resolvePersistedHistoryKey(key, effectiveAccountId)
573
+ : resolvePersistedHistoryKey(key, effectiveAccountId);
517
574
  const media: BncrPersistedConversationHistoryMediaEntry[] = [];
518
575
  const rawMedia = Array.isArray(entry?.media)
519
576
  ? (entry.media as Partial<BncrPersistedConversationHistoryMediaEntry>[])
@@ -534,13 +591,14 @@ export function createBncrStateStore(runtime: {
534
591
  const resolvedMessageId =
535
592
  messageId ||
536
593
  resolvePersistedOutboundReplayMessageId({
537
- bucketKey: key,
594
+ bucketKey: historyKey,
538
595
  sender,
539
596
  senderId: runtime.asString(entry?.senderId || '').trim(),
540
597
  body,
541
598
  timestamp,
542
599
  media,
543
600
  route,
601
+ accountId: effectiveAccountId,
544
602
  historyMap: options?.historyMap,
545
603
  });
546
604
  const normalizedMedia = media.map((item) => ({
@@ -549,7 +607,7 @@ export function createBncrStateStore(runtime: {
549
607
  ? { messageId: item.messageId || resolvedMessageId }
550
608
  : {}),
551
609
  }));
552
- entries.push({
610
+ const normalizedEntry = {
553
611
  sender,
554
612
  ...(runtime.asString(entry?.senderId || '').trim()
555
613
  ? { senderId: runtime.asString(entry?.senderId || '').trim() }
@@ -558,7 +616,7 @@ export function createBncrStateStore(runtime: {
558
616
  ...(timestamp > 0 ? { timestamp } : {}),
559
617
  ...(resolvedMessageId ? { messageId: resolvedMessageId } : {}),
560
618
  ...(normalizedMedia.length > 0 ? { media: normalizedMedia } : {}),
561
- ...(accountId ? { accountId } : {}),
619
+ accountId: effectiveAccountId,
562
620
  ...(sessionKey ? { sessionKey } : {}),
563
621
  ...(route ? { route } : {}),
564
622
  ...(type ? { type } : {}),
@@ -567,10 +625,13 @@ export function createBncrStateStore(runtime: {
567
625
  ...(status === 'pushed' || status === 'acked'
568
626
  ? { status: status as BncrOutboundReplayStatus }
569
627
  : {}),
570
- });
628
+ } satisfies BncrOutboundReplayEntry;
629
+ const entries = entriesByHistoryKey.get(historyKey) || [];
630
+ entries.push(normalizedEntry);
631
+ entriesByHistoryKey.set(historyKey, entries);
571
632
  }
572
- if (entries.length > 0) {
573
- const combined = options?.merge ? [...(target.get(key) || []), ...entries] : entries;
633
+ for (const [historyKey, entries] of entriesByHistoryKey) {
634
+ const combined = options?.merge ? [...(target.get(historyKey) || []), ...entries] : entries;
574
635
  const seenMessageIds = new Set<string>();
575
636
  const deduped: BncrOutboundReplayEntry[] = [];
576
637
  for (const entry of combined) {
@@ -580,7 +641,7 @@ export function createBncrStateStore(runtime: {
580
641
  deduped.push(entry);
581
642
  }
582
643
  if (deduped.length > 0) {
583
- target.set(key, normalizeOutboundReplayBucket(key, deduped));
644
+ target.set(historyKey, normalizeOutboundReplayBucket(historyKey, deduped));
584
645
  }
585
646
  }
586
647
  }