@xmoxmo/bncr 0.5.9 → 0.6.1

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.
@@ -3,6 +3,10 @@ import {
3
3
  normalizeRegisterDriftSnapshot,
4
4
  } from '../core/register-trace.ts';
5
5
  import type { BncrRoute, OutboxEntry } from '../core/types.ts';
6
+ import type {
7
+ BncrOutboundReplayEntry,
8
+ BncrOutboundReplayStatus,
9
+ } from '../messaging/inbound/outbound-replay-cache.ts';
6
10
  import {
7
11
  readOpenClawJsonFileWithFallback,
8
12
  writeOpenClawJsonFileAtomically,
@@ -14,6 +18,8 @@ import type {
14
18
  BncrPersistedGroupHistoryEntry,
15
19
  BncrPersistedGroupHistoryMediaEntry,
16
20
  BncrPersistedLastSession,
21
+ BncrPersistedOutboundReplayBucket,
22
+ BncrPersistedOutboundReplayEntry,
17
23
  BncrPersistedSessionRoute,
18
24
  PersistedState as BncrPersistedState,
19
25
  BncrSceneRecord,
@@ -28,6 +34,9 @@ type BncrPersistedStateStoreInput = {
28
34
  sessionRoutes?: unknown;
29
35
  sceneRegistry?: unknown;
30
36
  groupHistories?: unknown;
37
+ outboundReplayCache?: unknown;
38
+ // Legacy persisted key from before the outboundReplayCache rename.
39
+ messageCache?: unknown;
31
40
  lastSessionByAccount?: unknown;
32
41
  lastActivityByAccount?: unknown;
33
42
  lastInboundByAccount?: unknown;
@@ -41,6 +50,8 @@ type PersistedSessionRouteInput = Partial<BncrPersistedSessionRoute>;
41
50
  type PersistedSceneRecordInput = Partial<BncrSceneRecord>;
42
51
  type PersistedGroupHistoryBucketInput = Partial<BncrPersistedGroupHistoryBucket>;
43
52
  type PersistedGroupHistoryEntryInput = Partial<BncrPersistedGroupHistoryEntry>;
53
+ type PersistedOutboundReplayBucketInput = Partial<BncrPersistedOutboundReplayBucket>;
54
+ type PersistedOutboundReplayEntryInput = Partial<BncrOutboundReplayEntry>;
44
55
 
45
56
  export function createBncrStateStore(runtime: {
46
57
  getStatePath: () => string | null;
@@ -65,6 +76,7 @@ export function createBncrStateStore(runtime: {
65
76
  maxAccountActivityEntries: number;
66
77
  sceneRegistry: Map<string, BncrSceneRecord>;
67
78
  groupHistories: Map<string, BncrPersistedGroupHistoryEntry[]>;
79
+ outboundReplayCache?: Map<string, BncrPersistedOutboundReplayEntry[]>;
68
80
  outbox: Map<string, OutboxEntry>;
69
81
  getDeadLetter: () => OutboxEntry[];
70
82
  setDeadLetter: (entries: OutboxEntry[]) => void;
@@ -77,6 +89,9 @@ export function createBncrStateStore(runtime: {
77
89
  getLastDriftSnapshot: () => BncrPersistedState['lastDriftSnapshot'];
78
90
  setLastDriftSnapshot: (value: BncrPersistedState['lastDriftSnapshot']) => void;
79
91
  }) {
92
+ const outboundReplayCache =
93
+ runtime.outboundReplayCache ?? new Map<string, BncrPersistedOutboundReplayEntry[]>();
94
+
80
95
  function resolvePersistedGroupHistoryLimit(key: string): number {
81
96
  const scene = runtime.sceneRegistry.get(key);
82
97
  const sceneLimit = scene?.kind === 'group' ? scene.historyLimit : undefined;
@@ -148,8 +163,12 @@ export function createBncrStateStore(runtime: {
148
163
  return Array.from(runtime.sceneRegistry.values()).sort((a, b) => a.lastSeenAt - b.lastSeenAt);
149
164
  }
150
165
 
151
- function loadPersistedGroupHistories(persisted: unknown): void {
152
- runtime.groupHistories.clear();
166
+ function loadPersistedHistoryBuckets(
167
+ persisted: unknown,
168
+ target: Map<string, BncrPersistedGroupHistoryEntry[]>,
169
+ resolveLimit: (key: string) => number,
170
+ ): void {
171
+ target.clear();
153
172
  const buckets = Array.isArray(persisted)
154
173
  ? (persisted as PersistedGroupHistoryBucketInput[])
155
174
  : [];
@@ -199,15 +218,112 @@ export function createBncrStateStore(runtime: {
199
218
  entries.push(normalizedEntry);
200
219
  }
201
220
  if (entries.length > 0) {
202
- runtime.groupHistories.set(key, entries.slice(-resolvePersistedGroupHistoryLimit(key)));
221
+ const limit = resolveLimit(key);
222
+ target.set(key, Number.isFinite(limit) ? entries.slice(-limit) : entries);
203
223
  }
204
224
  }
205
225
  }
206
226
 
227
+ function loadPersistedGroupHistories(persisted: unknown): void {
228
+ loadPersistedHistoryBuckets(persisted, runtime.groupHistories, (key) =>
229
+ resolvePersistedGroupHistoryLimit(key),
230
+ );
231
+ }
232
+
233
+ function loadPersistedOutboundReplayCache(persisted: unknown): void {
234
+ outboundReplayCache.clear();
235
+ const buckets = Array.isArray(persisted)
236
+ ? (persisted as PersistedOutboundReplayBucketInput[])
237
+ : [];
238
+ for (const bucket of buckets) {
239
+ const key = runtime.asString(bucket?.key || '').trim();
240
+ if (!key) continue;
241
+ const entries: BncrOutboundReplayEntry[] = [];
242
+ const rawEntries = Array.isArray(bucket?.entries)
243
+ ? (bucket.entries as PersistedOutboundReplayEntryInput[])
244
+ : [];
245
+ for (const entry of rawEntries) {
246
+ const sender = runtime.asString(entry?.sender || '').trim();
247
+ const body = runtime.asString(entry?.body || '').trim();
248
+ if (!sender || !body) continue;
249
+ const timestamp = runtime.finiteNumberOr(entry?.timestamp, 0);
250
+ const messageId = runtime.asString(entry?.messageId || '').trim();
251
+ const accountId = runtime.asString(entry?.accountId || '').trim();
252
+ const sessionKey = runtime.asString(entry?.sessionKey || '').trim();
253
+ const type = runtime.asString(entry?.type || '').trim();
254
+ const mediaUrl = runtime.asString(entry?.mediaUrl || '').trim();
255
+ const createdAt = runtime.finiteNumberOr(entry?.createdAt, 0);
256
+ const status = runtime.asString(entry?.status || '').trim();
257
+ const route =
258
+ runtime.parseRouteLike(entry?.route) ||
259
+ runtime.parseRouteLike({
260
+ platform: entry?.route?.platform,
261
+ groupId: entry?.route?.groupId,
262
+ userId: entry?.route?.userId,
263
+ });
264
+ const media: BncrPersistedGroupHistoryMediaEntry[] = [];
265
+ const rawMedia = Array.isArray(entry?.media)
266
+ ? (entry.media as Partial<BncrPersistedGroupHistoryMediaEntry>[])
267
+ : [];
268
+ for (const item of rawMedia) {
269
+ const path = runtime.asString(item?.path || '').trim();
270
+ if (!path) continue;
271
+ const contentType = runtime.asString(item?.contentType || '').trim();
272
+ const kind = runtime.asString(item?.kind || '').trim();
273
+ const mediaMessageId = runtime.asString(item?.messageId || '').trim();
274
+ media.push({
275
+ path,
276
+ ...(contentType ? { contentType } : {}),
277
+ ...(kind ? { kind: kind as BncrPersistedGroupHistoryMediaEntry['kind'] } : {}),
278
+ ...(mediaMessageId ? { messageId: mediaMessageId } : {}),
279
+ });
280
+ }
281
+ entries.push({
282
+ sender,
283
+ ...(runtime.asString(entry?.senderId || '').trim()
284
+ ? { senderId: runtime.asString(entry?.senderId || '').trim() }
285
+ : {}),
286
+ body,
287
+ ...(timestamp > 0 ? { timestamp } : {}),
288
+ ...(messageId ? { messageId } : {}),
289
+ ...(media.length > 0 ? { media } : {}),
290
+ ...(accountId ? { accountId } : {}),
291
+ ...(sessionKey ? { sessionKey } : {}),
292
+ ...(route ? { route } : {}),
293
+ ...(type ? { type } : {}),
294
+ ...(mediaUrl ? { mediaUrl } : {}),
295
+ ...(createdAt > 0 ? { createdAt } : {}),
296
+ ...(status === 'pushed' || status === 'acked'
297
+ ? { status: status as BncrOutboundReplayStatus }
298
+ : {}),
299
+ });
300
+ }
301
+ if (entries.length > 0) {
302
+ outboundReplayCache.set(key, entries);
303
+ }
304
+ }
305
+ }
306
+
307
+ function dumpPersistedHistoryBuckets(
308
+ source: Map<string, BncrPersistedGroupHistoryEntry[]>,
309
+ resolveLimit: (key: string) => number,
310
+ ) {
311
+ return Array.from(source.entries()).map(([key, entries]) => ({
312
+ key,
313
+ entries: Number.isFinite(resolveLimit(key)) ? entries.slice(-resolveLimit(key)) : entries,
314
+ }));
315
+ }
316
+
207
317
  function dumpPersistedGroupHistories() {
208
- return Array.from(runtime.groupHistories.entries()).map(([key, entries]) => ({
318
+ return dumpPersistedHistoryBuckets(runtime.groupHistories, (key) =>
319
+ resolvePersistedGroupHistoryLimit(key),
320
+ );
321
+ }
322
+
323
+ function dumpPersistedOutboundReplayCache() {
324
+ return Array.from(outboundReplayCache.entries()).map(([key, entries]) => ({
209
325
  key,
210
- entries: entries.slice(-resolvePersistedGroupHistoryLimit(key)),
326
+ entries,
211
327
  }));
212
328
  }
213
329
 
@@ -359,6 +475,7 @@ export function createBncrStateStore(runtime: {
359
475
  loadPersistedSessionRoutes(data.sessionRoutes);
360
476
  loadPersistedSceneRegistry(data.sceneRegistry);
361
477
  loadPersistedGroupHistories(data.groupHistories);
478
+ loadPersistedOutboundReplayCache(data.outboundReplayCache ?? data.messageCache);
362
479
  loadPersistedLastSessionMap(data.lastSessionByAccount);
363
480
  loadPersistedAccountTimestampMap(runtime.lastActivityByAccount, data.lastActivityByAccount);
364
481
  loadPersistedAccountTimestampMap(runtime.lastInboundByAccount, data.lastInboundByAccount);
@@ -378,6 +495,7 @@ export function createBncrStateStore(runtime: {
378
495
  sessionRoutes: dumpPersistedSessionRoutes(),
379
496
  sceneRegistry: dumpPersistedSceneRegistry(),
380
497
  groupHistories: dumpPersistedGroupHistories(),
498
+ outboundReplayCache: dumpPersistedOutboundReplayCache(),
381
499
  lastSessionByAccount: dumpPersistedLastSessionMap(),
382
500
  lastActivityByAccount: dumpPersistedAccountTimestampMap(runtime.lastActivityByAccount),
383
501
  lastInboundByAccount: dumpPersistedAccountTimestampMap(runtime.lastInboundByAccount),
@@ -399,6 +517,8 @@ export function createBncrStateStore(runtime: {
399
517
  dumpPersistedSceneRegistry,
400
518
  loadPersistedGroupHistories,
401
519
  dumpPersistedGroupHistories,
520
+ loadPersistedOutboundReplayCache,
521
+ dumpPersistedOutboundReplayCache,
402
522
  backfillAccountActivityFromSessionRoutes,
403
523
  loadState,
404
524
  flushState,
@@ -6,7 +6,11 @@ import type {
6
6
  FileSendTransferState,
7
7
  OutboxEntry,
8
8
  } from '../core/types.ts';
9
- import type { BncrPersistedGroupHistoryEntry, BncrSceneRecord } from './channel-runtime-types.ts';
9
+ import type {
10
+ BncrPersistedGroupHistoryEntry,
11
+ BncrPersistedOutboundReplayEntry,
12
+ BncrSceneRecord,
13
+ } from './channel-runtime-types.ts';
10
14
  import { createBncrStateStore } from './state-store.ts';
11
15
  import { createBncrTransientStateRuntime } from './transient-state-runtime.ts';
12
16
 
@@ -34,6 +38,7 @@ export function createBncrStateTransientRuntimeGroup(runtime: {
34
38
  maxAccountActivityEntries: number;
35
39
  sceneRegistry: Map<string, BncrSceneRecord>;
36
40
  groupHistories: Map<string, BncrPersistedGroupHistoryEntry[]>;
41
+ outboundReplayCache?: Map<string, BncrPersistedOutboundReplayEntry[]>;
37
42
  outbox: Map<string, OutboxEntry>;
38
43
  getDeadLetter: () => OutboxEntry[];
39
44
  setDeadLetter: (entries: OutboxEntry[]) => void;
@@ -74,6 +79,8 @@ export function createBncrStateTransientRuntimeGroup(runtime: {
74
79
  maxAccountActivityEntries: runtime.maxAccountActivityEntries,
75
80
  sceneRegistry: runtime.sceneRegistry,
76
81
  groupHistories: runtime.groupHistories,
82
+ outboundReplayCache:
83
+ runtime.outboundReplayCache ?? new Map<string, BncrPersistedOutboundReplayEntry[]>(),
77
84
  outbox: runtime.outbox,
78
85
  getDeadLetter: runtime.getDeadLetter,
79
86
  setDeadLetter: runtime.setDeadLetter,