@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.
Files changed (31) hide show
  1. package/README.md +65 -1
  2. package/package.json +1 -1
  3. package/scripts/check-pack.mjs +4 -1
  4. package/scripts/selfcheck.mjs +4 -1
  5. package/src/channel.ts +144 -5
  6. package/src/messaging/inbound/commands.ts +62 -26
  7. package/src/messaging/inbound/context-facts.ts +88 -58
  8. package/src/messaging/inbound/conversation-history-serial.ts +258 -0
  9. package/src/messaging/inbound/conversation-history.ts +450 -0
  10. package/src/messaging/inbound/dispatch.ts +568 -123
  11. package/src/messaging/inbound/history-shard-worker.ts +180 -0
  12. package/src/messaging/inbound/native-command.ts +89 -33
  13. package/src/messaging/inbound/outbound-replay-cache.ts +202 -17
  14. package/src/messaging/inbound/reply-dispatch.ts +7 -6
  15. package/src/messaging/inbound/scene-admin.ts +40 -20
  16. package/src/messaging/inbound/turn-context.ts +101 -37
  17. package/src/plugin/bridge-lifecycle.ts +8 -0
  18. package/src/plugin/bridge-outbox-facade.ts +15 -1
  19. package/src/plugin/channel-components.ts +5 -5
  20. package/src/plugin/channel-runtime-builders-delivery.ts +2 -2
  21. package/src/plugin/channel-runtime-constants.ts +2 -0
  22. package/src/plugin/channel-runtime-types.ts +9 -7
  23. package/src/plugin/inbound-acceptance.ts +9 -24
  24. package/src/plugin/inbound-handlers.ts +4 -4
  25. package/src/plugin/inbound-surface-handlers-group.ts +4 -2
  26. package/src/plugin/outbox-drain-post-push.ts +3 -0
  27. package/src/plugin/scene-registry.ts +4 -4
  28. package/src/plugin/sqlite-state.ts +2109 -0
  29. package/src/plugin/state-store.ts +731 -69
  30. package/src/plugin/state-transient-runtime-group.ts +17 -3
  31. package/src/messaging/inbound/group-history.ts +0 -264
@@ -1,3 +1,5 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
1
3
  import type { RegisterDriftSnapshot } from '../core/register-trace.ts';
2
4
  import type {
3
5
  BncrConnection,
@@ -7,7 +9,7 @@ import type {
7
9
  OutboxEntry,
8
10
  } from '../core/types.ts';
9
11
  import type {
10
- BncrPersistedGroupHistoryEntry,
12
+ BncrPersistedConversationHistoryEntry,
11
13
  BncrPersistedOutboundReplayEntry,
12
14
  BncrSceneRecord,
13
15
  } from './channel-runtime-types.ts';
@@ -17,6 +19,13 @@ import { createBncrTransientStateRuntime } from './transient-state-runtime.ts';
17
19
  type StoredRouteRecord = { accountId: string; route: BncrRoute; updatedAt: number };
18
20
  type StoredLastSessionRecord = { sessionKey: string; scope: string; updatedAt: number };
19
21
 
22
+ export function shouldEnableBncrSqliteState(statePath: string | null): boolean {
23
+ if (!statePath) return false;
24
+ if (process.env.BNCR_SQLITE_STORE === '0') return false;
25
+ if (process.env.BNCR_SQLITE_STORE === '1') return true;
26
+ return existsSync(path.join(path.dirname(statePath), 'bncr.sqlite3'));
27
+ }
28
+
20
29
  export function createBncrStateTransientRuntimeGroup(runtime: {
21
30
  bridgeId: string;
22
31
  getStatePath: () => string | null;
@@ -37,7 +46,7 @@ export function createBncrStateTransientRuntimeGroup(runtime: {
37
46
  maxSessionRouteEntries: number;
38
47
  maxAccountActivityEntries: number;
39
48
  sceneRegistry: Map<string, BncrSceneRecord>;
40
- groupHistories: Map<string, BncrPersistedGroupHistoryEntry[]>;
49
+ conversationHistories: Map<string, BncrPersistedConversationHistoryEntry[]>;
41
50
  outboundReplayCache?: Map<string, BncrPersistedOutboundReplayEntry[]>;
42
51
  outbox: Map<string, OutboxEntry>;
43
52
  getDeadLetter: () => OutboxEntry[];
@@ -78,7 +87,7 @@ export function createBncrStateTransientRuntimeGroup(runtime: {
78
87
  maxSessionRouteEntries: runtime.maxSessionRouteEntries,
79
88
  maxAccountActivityEntries: runtime.maxAccountActivityEntries,
80
89
  sceneRegistry: runtime.sceneRegistry,
81
- groupHistories: runtime.groupHistories,
90
+ conversationHistories: runtime.conversationHistories,
82
91
  outboundReplayCache:
83
92
  runtime.outboundReplayCache ?? new Map<string, BncrPersistedOutboundReplayEntry[]>(),
84
93
  outbox: runtime.outbox,
@@ -92,6 +101,11 @@ export function createBncrStateTransientRuntimeGroup(runtime: {
92
101
  lastOutboundByAccount: runtime.lastOutboundByAccount,
93
102
  getLastDriftSnapshot: runtime.getLastDriftSnapshot,
94
103
  setLastDriftSnapshot: runtime.setLastDriftSnapshot,
104
+ createSqliteState: async (statePath) => {
105
+ if (!statePath || !shouldEnableBncrSqliteState(statePath)) return null;
106
+ const { createBncrSqliteStateDatabase } = await import('./sqlite-state.ts');
107
+ return createBncrSqliteStateDatabase(path.join(path.dirname(statePath), 'bncr.sqlite3'));
108
+ },
95
109
  });
96
110
 
97
111
  const transientStateRuntime = createBncrTransientStateRuntime({
@@ -1,264 +0,0 @@
1
- import {
2
- createChannelHistoryWindow,
3
- DEFAULT_GROUP_HISTORY_LIMIT,
4
- type HistoryEntry,
5
- type HistoryMediaEntry,
6
- } from 'openclaw/plugin-sdk/reply-history';
7
- import { formatOpenClawAgentEnvelope } from '../../openclaw/reply-runtime.ts';
8
- import type { BncrInboundApi } from './contracts.ts';
9
- import type { ParsedInbound } from './dispatch-prep.ts';
10
-
11
- export type BncrHistoryEntry = HistoryEntry & {
12
- senderId?: string;
13
- };
14
-
15
- export type BncrGroupHistoryMap = Map<string, BncrHistoryEntry[]>;
16
-
17
- type BncrHistoryMediaKind = NonNullable<HistoryMediaEntry['kind']>;
18
-
19
- function normalizeTextBody(value: string): string {
20
- return String(value || '')
21
- .replace(/\s+/g, ' ')
22
- .trim();
23
- }
24
-
25
- export function buildBncrGroupHistoryKey(parsed: ParsedInbound): string | null {
26
- if (parsed.peer.kind !== 'group') return null;
27
- const platform = String(parsed.platform || '').trim();
28
- const groupId = String(parsed.groupId || '').trim();
29
- if (!platform || !groupId || groupId === '0') return null;
30
- return `${platform}:${groupId}`;
31
- }
32
-
33
- export function recordBncrPendingGroupText(args: {
34
- historyMap: BncrGroupHistoryMap;
35
- parsed: ParsedInbound;
36
- senderDisplayName: string;
37
- senderId: string;
38
- bodyText: string;
39
- historyLimit?: number;
40
- }) {
41
- const limit =
42
- typeof args.historyLimit === 'number' &&
43
- Number.isFinite(args.historyLimit) &&
44
- args.historyLimit >= 0
45
- ? Math.floor(args.historyLimit)
46
- : DEFAULT_GROUP_HISTORY_LIMIT;
47
- const historyKey = buildBncrGroupHistoryKey(args.parsed);
48
- const body = normalizeTextBody(args.bodyText);
49
- if (!historyKey || !body || args.parsed.msgType !== 'text') return;
50
- const msgId = String(args.parsed.msgId || '').trim();
51
- if (msgId) {
52
- const existing = args.historyMap.get(historyKey) || [];
53
- if (existing.some((e) => e.messageId === msgId)) return;
54
- }
55
- createChannelHistoryWindow({ historyMap: args.historyMap }).record({
56
- historyKey,
57
- limit,
58
- entry: {
59
- sender: args.senderDisplayName,
60
- senderId: args.senderId,
61
- body,
62
- timestamp: Date.now(),
63
- messageId: args.parsed.msgId,
64
- },
65
- });
66
- }
67
-
68
- function inferBncrHistoryMediaKind(args: {
69
- msgType?: string;
70
- mediaContentType?: string;
71
- }): BncrHistoryMediaKind {
72
- const msgType = String(args.msgType || '')
73
- .trim()
74
- .toLowerCase();
75
- const contentType = String(args.mediaContentType || '')
76
- .trim()
77
- .toLowerCase();
78
- if (msgType === 'image' || contentType.startsWith('image/')) return 'image';
79
- if (msgType === 'video' || contentType.startsWith('video/')) return 'video';
80
- if (msgType === 'audio' || msgType === 'voice' || contentType.startsWith('audio/'))
81
- return 'audio';
82
- if (msgType === 'file' || msgType === 'document') return 'document';
83
- if (contentType) return 'document';
84
- return 'unknown';
85
- }
86
-
87
- function buildBncrHistoryMediaBody(kind: BncrHistoryMediaKind): string {
88
- return `<media:${kind}>`;
89
- }
90
-
91
- export async function recordBncrPendingGroupMedia(args: {
92
- historyMap: BncrGroupHistoryMap;
93
- parsed: ParsedInbound;
94
- senderDisplayName: string;
95
- senderId: string;
96
- bodyText: string;
97
- mediaItems?: Array<{
98
- path: string;
99
- contentType?: string;
100
- kind?: BncrHistoryMediaKind;
101
- }>;
102
- mediaContentType?: string;
103
- historyLimit?: number;
104
- }) {
105
- const limit =
106
- typeof args.historyLimit === 'number' &&
107
- Number.isFinite(args.historyLimit) &&
108
- args.historyLimit >= 0
109
- ? Math.floor(args.historyLimit)
110
- : DEFAULT_GROUP_HISTORY_LIMIT;
111
- const historyKey = buildBncrGroupHistoryKey(args.parsed);
112
- if (!historyKey || args.parsed.msgType === 'text') return;
113
- const normalizedMediaItems = Array.isArray(args.mediaItems) ? args.mediaItems : [];
114
- const itemKinds = normalizedMediaItems
115
- .map((item) =>
116
- String(item?.kind || '')
117
- .trim()
118
- .toLowerCase(),
119
- )
120
- .filter(Boolean);
121
- const kind = itemKinds[0]
122
- ? itemKinds.every((candidate) => candidate === itemKinds[0])
123
- ? (itemKinds[0] as BncrHistoryMediaKind)
124
- : 'document'
125
- : inferBncrHistoryMediaKind({
126
- msgType: args.parsed.msgType,
127
- mediaContentType: args.mediaContentType,
128
- });
129
- const body = normalizeTextBody(args.bodyText) || buildBncrHistoryMediaBody(kind);
130
- if (normalizedMediaItems.length === 0) {
131
- const msgId = String(args.parsed.msgId || '').trim();
132
- if (msgId) {
133
- const existing = args.historyMap.get(historyKey) || [];
134
- if (existing.some((e) => e.messageId === msgId)) return;
135
- }
136
- createChannelHistoryWindow({ historyMap: args.historyMap }).record({
137
- historyKey,
138
- limit,
139
- entry: {
140
- sender: args.senderDisplayName,
141
- senderId: args.senderId,
142
- body,
143
- timestamp: Date.now(),
144
- messageId: args.parsed.msgId,
145
- },
146
- });
147
- return;
148
- }
149
- const msgId = String(args.parsed.msgId || '').trim();
150
- if (msgId) {
151
- const existing = args.historyMap.get(historyKey) || [];
152
- if (existing.some((e) => e.messageId === msgId)) return;
153
- }
154
- createChannelHistoryWindow({ historyMap: args.historyMap }).record({
155
- historyKey,
156
- limit,
157
- entry: {
158
- sender: args.senderDisplayName,
159
- senderId: args.senderId,
160
- body,
161
- timestamp: Date.now(),
162
- messageId: args.parsed.msgId,
163
- media: normalizedMediaItems.map(
164
- (item) =>
165
- ({
166
- path: item.path,
167
- contentType: item.contentType || args.mediaContentType,
168
- kind: item.kind || kind,
169
- messageId: args.parsed.msgId,
170
- }) satisfies HistoryMediaEntry,
171
- ),
172
- },
173
- });
174
- }
175
-
176
- function cloneBncrHistoryEntry(entry: BncrHistoryEntry): BncrHistoryEntry {
177
- return {
178
- sender: entry.sender,
179
- ...(entry.messageId ? { messageId: entry.messageId } : {}),
180
- ...(typeof entry.timestamp === 'number' ? { timestamp: entry.timestamp } : {}),
181
- ...(entry.senderId ? { senderId: entry.senderId } : {}),
182
- body: entry.body,
183
- ...(Array.isArray(entry.media)
184
- ? {
185
- media: entry.media.map((item) => ({
186
- ...(item?.path ? { path: item.path } : {}),
187
- ...(item?.contentType ? { contentType: item.contentType } : {}),
188
- ...(item?.kind ? { kind: item.kind } : {}),
189
- ...(item?.messageId ? { messageId: item.messageId } : {}),
190
- })),
191
- }
192
- : {}),
193
- };
194
- }
195
-
196
- export function readBncrPendingGroupHistorySnapshot(args: {
197
- historyMap: BncrGroupHistoryMap;
198
- parsed: ParsedInbound;
199
- historyLimit?: number;
200
- }): BncrHistoryEntry[] {
201
- const limit =
202
- typeof args.historyLimit === 'number' &&
203
- Number.isFinite(args.historyLimit) &&
204
- args.historyLimit >= 0
205
- ? Math.floor(args.historyLimit)
206
- : DEFAULT_GROUP_HISTORY_LIMIT;
207
- const historyKey = buildBncrGroupHistoryKey(args.parsed);
208
- if (!historyKey) return [];
209
- const entries = args.historyMap.get(historyKey) || [];
210
- if (entries.length === 0) return [];
211
- return entries.slice(-limit).map(cloneBncrHistoryEntry);
212
- }
213
-
214
- export function buildBncrGroupContextFromEntries(args: {
215
- api: BncrInboundApi;
216
- entries: readonly BncrHistoryEntry[];
217
- channelLabel: string;
218
- currentTimestamp: number;
219
- previousTimestamp?: unknown;
220
- envelope?: unknown;
221
- currentMessage: string;
222
- }) {
223
- if (args.entries.length === 0) return args.currentMessage;
224
- const historyText = args.entries
225
- .map((entry) =>
226
- formatOpenClawAgentEnvelope(args.api, {
227
- channel: 'Bncr',
228
- from:
229
- (entry.senderId
230
- ? `${entry.sender} (${entry.senderId})`
231
- : entry.sender || args.channelLabel) + (entry.messageId ? ` #${entry.messageId}` : ''),
232
- timestamp: entry.timestamp || args.currentTimestamp,
233
- previousTimestamp: args.previousTimestamp,
234
- envelope: args.envelope,
235
- body: entry.body,
236
- }),
237
- )
238
- .join('\n');
239
- return `[Chat messages since your last reply - for context]\n${historyText}\n\n[Current message - respond to this]${args.currentMessage}`;
240
- }
241
-
242
- export function collectBncrHistoryMediaFromEntries(args: {
243
- entries: readonly BncrHistoryEntry[];
244
- }): HistoryMediaEntry[] {
245
- return args.entries.flatMap((entry) => (Array.isArray(entry.media) ? entry.media : []));
246
- }
247
- export function clearBncrPendingGroupHistory(args: {
248
- historyMap: BncrGroupHistoryMap;
249
- parsed: ParsedInbound;
250
- historyLimit?: number;
251
- }) {
252
- const limit =
253
- typeof args.historyLimit === 'number' &&
254
- Number.isFinite(args.historyLimit) &&
255
- args.historyLimit >= 0
256
- ? Math.floor(args.historyLimit)
257
- : DEFAULT_GROUP_HISTORY_LIMIT;
258
- const historyKey = buildBncrGroupHistoryKey(args.parsed);
259
- if (!historyKey) return;
260
- createChannelHistoryWindow({ historyMap: args.historyMap }).clear({
261
- historyKey,
262
- limit: limit,
263
- });
264
- }