@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.
@@ -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
 
@@ -52,9 +53,9 @@ export const HISTORY_HELP_TEXT = [
52
53
  '📋 Bncr Conversation History Configuration',
53
54
  '',
54
55
  'Commands:',
55
- ' • /bncr history-limit <number> [<SceneId>]',
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');
@@ -258,7 +259,7 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
258
259
  return {
259
260
  matched: true,
260
261
  valid: false,
261
- text: 'Usage: /bncr history-limit [<number>] [<sceneKey>]',
262
+ text: 'Usage: /bncr history-limit [<number>|clear] [<sceneKey>]',
262
263
  };
263
264
  case 'history-force':
264
265
  if (args.length === 0) {
@@ -353,12 +354,11 @@ function formatSceneDetailsLine(scene: BncrSceneRecord): string {
353
354
  scene.kind === 'group' ? scene.groupId || scene.sceneKey : scene.userId || scene.sceneKey;
354
355
  const namePart = scene.kind === 'group' ? scene.groupName || '' : scene.userName || '';
355
356
  const labelPart = namePart ? ` name=${namePart}` : '';
356
- const historyLimitVal = scene.historyLimit;
357
+ const historyLimitVal = resolveBncrHistoryLimit(scene.historyLimit);
357
358
  const historyForceVal = scene.historyForce;
358
- const hasNonDefaultHistory =
359
- (typeof historyLimitVal === 'number' && historyLimitVal !== 50) || historyForceVal === false;
359
+ const hasNonDefaultHistory = historyLimitVal !== 50 || historyForceVal === false;
360
360
  const historyPart = hasNonDefaultHistory
361
- ? ` historyLimit=${historyLimitVal ?? 50} autoFlush=${historyForceVal !== false ? 'on' : 'off'}`
361
+ ? ` historyLimit=${historyLimitVal} autoFlush=${historyForceVal !== false ? 'on' : 'off'}`
362
362
  : '';
363
363
  return ` Details: status=${scene.status} id=${idPart}${labelPart}${historyPart}`;
364
364
  }
@@ -496,7 +496,10 @@ export function executeSceneAdminCommand(args: {
496
496
  }
497
497
  const s = sceneRegistry.get(qSceneKey);
498
498
  if (!s) return { ok: true, text: 'Default history limit is 50.' };
499
- return { ok: true, text: `Current ${qSceneKey} history limit is ${s.historyLimit ?? 50}.` };
499
+ return {
500
+ ok: true,
501
+ text: `Current ${qSceneKey} history limit is ${resolveBncrHistoryLimit(s.historyLimit)}.`,
502
+ };
500
503
  }
501
504
 
502
505
  if (command.kind === 'history-limit-set') {
@@ -518,12 +521,14 @@ export function executeSceneAdminCommand(args: {
518
521
  let resolvedLimit = rawLimit;
519
522
  if (resolvedLimit >= 51) {
520
523
  resolvedLimit = Math.min(Math.floor(resolvedLimit), 10000);
521
- } else if (resolvedLimit < 0 && Math.abs(resolvedLimit) >= 3) {
522
- resolvedLimit = Math.floor(Math.abs(resolvedLimit));
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);
523
528
  } else {
524
529
  return {
525
530
  ok: false,
526
- text: `Value too small, must be >= 51, or use negative number (abs >= 3) for hidden override.`,
531
+ text: `Value too small, must be >= 2, or use negative number (abs >= 2) for hidden override.`,
527
532
  };
528
533
  }
529
534
  sceneRegistry.set(sSceneKey, {
@@ -9,11 +9,7 @@ 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
+ import type { BncrConversationHistoryMap, BncrHistoryEntry } from './conversation-history.ts';
17
13
  import type {
18
14
  BncrInboundConversationResolution,
19
15
  BncrPreparedInboundSessionContext,
@@ -193,9 +189,6 @@ export function buildBncrInboundTurnContext(args: {
193
189
  const senderIsOwner = parsed.isAdmin === true || Boolean(ownerAllowFrom?.length);
194
190
  const senderIsAuthorized = senderIsOwner;
195
191
  const inboundHistory = undefined;
196
- const contextMedia = shouldDispatch
197
- ? collectBncrHistoryMediaFromEntries({ entries: contextEntries })
198
- : [];
199
192
  const structuredContextEntries = shouldDispatch
200
193
  ? contextEntries.map((entry) => ({
201
194
  messageId: entry.messageId,
@@ -271,7 +264,7 @@ export function buildBncrInboundTurnContext(args: {
271
264
  kind: item.kind,
272
265
  messageId: msgId ?? undefined,
273
266
  }));
274
- const media = dedupeBncrTurnMedia([...contextMedia, ...currentTurnMedia]);
267
+ const media = dedupeBncrTurnMedia(currentTurnMedia);
275
268
 
276
269
  return Promise.resolve(
277
270
  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);
@@ -45,7 +45,9 @@ export function createBncrBridgeOutboxFacade(runtime: {
45
45
  outboundReplayCache?: BncrOutboundReplayCache;
46
46
  conversationHistories?: BncrConversationHistoryMap;
47
47
  resolveOutboundHistoryLimit?: (entry: OutboxEntry) => number;
48
+ resolveOutboundHistoryForce?: (entry: OutboxEntry) => boolean;
48
49
  resolveOutboundSender?: (entry: OutboxEntry) => { sender: string; senderId?: string };
50
+ onConversationHistoryOverflow?: (entry: OutboxEntry, historyVersion?: number) => void;
49
51
  isOutboundAckRequired?: (accountId: string) => boolean;
50
52
  scheduleSave: () => void;
51
53
  flushPushQueueBestEffort: (args?: {
@@ -65,7 +67,7 @@ export function createBncrBridgeOutboxFacade(runtime: {
65
67
  sender: entry.accountId,
66
68
  senderId: entry.accountId,
67
69
  };
68
- recordBncrOutboundReplay({
70
+ const replayResult = recordBncrOutboundReplay({
69
71
  cache: runtime.outboundReplayCache,
70
72
  ...(runtime.conversationHistories
71
73
  ? { conversationHistories: runtime.conversationHistories }
@@ -78,6 +80,9 @@ export function createBncrBridgeOutboxFacade(runtime: {
78
80
  senderId: senderInfo.senderId,
79
81
  status,
80
82
  });
83
+ if (replayResult.historyOverflow && runtime.resolveOutboundHistoryForce?.(entry) !== false) {
84
+ runtime.onConversationHistoryOverflow?.(entry, replayResult.historyVersion);
85
+ }
81
86
  };
82
87
 
83
88
  const markRecentOutboundAcked = (entry: OutboxEntry) => {
@@ -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;
@@ -95,6 +95,9 @@ export function createBncrOutboxDrainPostPush(runtime: BncrOutboxDrainPostPushRu
95
95
  ackTimeoutMs,
96
96
  });
97
97
 
98
+ if (!requireAck && onlineNow) {
99
+ runtime.outbox.delete(entry.messageId);
100
+ }
98
101
  if (!runtime.outbox.has(entry.messageId)) {
99
102
  await runtime.sleepMs(runtime.pushDrainIntervalMs);
100
103
  return { action: 'continue', localNextDelay };