@xmoxmo/bncr 0.2.4 → 0.2.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.
Files changed (41) hide show
  1. package/README.md +68 -5
  2. package/package.json +1 -1
  3. package/src/channel.ts +2985 -1706
  4. package/src/core/connection-capability.ts +70 -0
  5. package/src/core/connection-reachability.ts +168 -0
  6. package/src/core/diagnostics.ts +54 -0
  7. package/src/core/downlink-health.ts +61 -0
  8. package/src/core/extended-diagnostics.ts +65 -0
  9. package/src/core/lease-state.ts +94 -0
  10. package/src/core/outbox-enqueue.ts +22 -0
  11. package/src/core/outbox-entry-builders.ts +92 -0
  12. package/src/core/outbox-file-transfer-bookkeeping.ts +31 -0
  13. package/src/core/outbox-file-transfer-failure.ts +25 -0
  14. package/src/core/outbox-file-transfer-guards.ts +66 -0
  15. package/src/core/outbox-file-transfer-prep.ts +31 -0
  16. package/src/core/outbox-file-transfer-success.ts +34 -0
  17. package/src/core/outbox-push-args.ts +67 -0
  18. package/src/core/outbox-queue.ts +69 -0
  19. package/src/core/outbox-summary.ts +14 -0
  20. package/src/core/outbox-text-push-failure.ts +10 -0
  21. package/src/core/outbox-text-push-guards.ts +51 -0
  22. package/src/core/outbox-text-push-prep.ts +36 -0
  23. package/src/core/outbox-text-push-success.ts +62 -0
  24. package/src/core/policy.ts +9 -0
  25. package/src/core/register-trace.ts +115 -0
  26. package/src/core/status.ts +57 -0
  27. package/src/core/types.ts +1 -0
  28. package/src/messaging/inbound/commands.ts +318 -75
  29. package/src/messaging/inbound/dispatch.ts +435 -139
  30. package/src/messaging/inbound/parse.ts +8 -0
  31. package/src/messaging/inbound/session-label.ts +115 -0
  32. package/src/messaging/outbound/diagnostics.ts +262 -0
  33. package/src/messaging/outbound/media-dedupe.ts +51 -0
  34. package/src/messaging/outbound/media.ts +3 -1
  35. package/src/messaging/outbound/queue-selectors.ts +191 -0
  36. package/src/messaging/outbound/reasons.ts +52 -0
  37. package/src/messaging/outbound/reply-enqueue.ts +329 -0
  38. package/src/messaging/outbound/reply-target-policy.ts +13 -0
  39. package/src/messaging/outbound/retry-policy.ts +142 -0
  40. package/src/messaging/outbound/send.ts +6 -0
  41. package/src/messaging/outbound/session-route.ts +34 -5
@@ -1,10 +1,18 @@
1
+ import { resolvePinnedMainDmOwnerFromAllowlist } from 'openclaw/plugin-sdk/conversation-runtime';
2
+ import { resolveInboundLastRouteSessionKey } from 'openclaw/plugin-sdk/routing';
1
3
  import { emitBncrLogLine } from '../../core/logging.ts';
4
+ import { resolveBncrChannelPolicy } from '../../core/policy.ts';
2
5
  import {
3
6
  formatDisplayScope,
4
7
  normalizeInboundSessionKey,
5
8
  withTaskSessionKey,
6
9
  } from '../../core/targets.ts';
7
10
  import { buildBncrReplyConfig } from './reply-config.ts';
11
+ import {
12
+ buildBncrInboundSessionIdentityPatch,
13
+ recordAndPatchBncrInboundSessionEntry,
14
+ wrapBncrInboundRecordSessionLabelCorrection,
15
+ } from './session-label.ts';
8
16
 
9
17
  type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
10
18
 
@@ -14,6 +22,33 @@ type NativeCommand = {
14
22
  body: string;
15
23
  };
16
24
 
25
+ type NativeVerboseCommand = {
26
+ handled: true;
27
+ verboseLevel?: 'on' | 'off' | 'full';
28
+ text: string;
29
+ };
30
+
31
+ function resolveBncrNativeVerboseCommand(command: NativeCommand): NativeVerboseCommand | null {
32
+ if (command.command !== 'verbose') return null;
33
+ const rawLevel = String(command.raw.slice('/verbose'.length) || '').trim().toLowerCase();
34
+ if (!rawLevel || rawLevel === 'status') {
35
+ return { handled: true, text: 'Current verbose level is unchanged.' };
36
+ }
37
+ if (rawLevel === 'on') return { handled: true, verboseLevel: 'on', text: 'Verbose logging enabled.' };
38
+ if (rawLevel === 'off') return { handled: true, verboseLevel: 'off', text: 'Verbose logging disabled.' };
39
+ if (rawLevel === 'full') return { handled: true, verboseLevel: 'full', text: 'Verbose logging set to full.' };
40
+ return { handled: true, text: `Unrecognized verbose level "${rawLevel}". Valid levels: off, on, full.` };
41
+ }
42
+
43
+ function logBncrNativeCommandEvent(
44
+ event: string,
45
+ fields: Record<string, unknown>,
46
+ options?: { debugOnly?: boolean; debugEnabled?: boolean },
47
+ ) {
48
+ if (options?.debugOnly && !options?.debugEnabled) return;
49
+ emitBncrLogLine('info', `[bncr] native-command ${JSON.stringify({ event, ...fields })}`);
50
+ }
51
+
17
52
  export function parseBncrNativeCommand(text: string): NativeCommand | null {
18
53
  const raw = String(text || '').trim();
19
54
  if (!raw.startsWith('/')) return null;
@@ -45,7 +80,10 @@ export async function handleBncrNativeCommand(params: {
45
80
  mediaLocalRoots?: readonly string[];
46
81
  }) => Promise<void>;
47
82
  logger?: { warn?: (msg: string) => void; error?: (msg: string) => void };
48
- }): Promise<{ handled: false } | { handled: true; command: string; sessionKey: string }> {
83
+ }): Promise<
84
+ | { handled: false }
85
+ | { handled: true; command: string; sessionKey: string; fallbackToAgent?: boolean }
86
+ > {
49
87
  const {
50
88
  api,
51
89
  channelId,
@@ -56,9 +94,21 @@ export async function handleBncrNativeCommand(params: {
56
94
  enqueueFromReply,
57
95
  logger,
58
96
  } = params;
59
- const { accountId, route, peer, sessionKeyfromroute, clientId, extracted, msgId } = parsed;
97
+ const { accountId, route, peer, sessionKeyfromroute, providedOriginatingTo, clientId, extracted, msgId } = parsed;
60
98
  const command = parseBncrNativeCommand(extracted.text);
61
99
  if (!command) return { handled: false };
100
+ const nativeCommandDebugEnabled = cfg?.channels?.[channelId]?.debug?.verbose === true;
101
+
102
+ logBncrNativeCommandEvent(
103
+ 'detected',
104
+ {
105
+ command: command.command,
106
+ accountId,
107
+ to: formatDisplayScope(route),
108
+ msgId: msgId || null,
109
+ },
110
+ { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
111
+ );
62
112
 
63
113
  const resolvedRoute = api.runtime.channel.routing.resolveAgentRoute({
64
114
  cfg,
@@ -77,102 +127,295 @@ export async function handleBncrNativeCommand(params: {
77
127
  rememberSessionRoute(taskSessionKey, accountId, route);
78
128
 
79
129
  const displayTo = formatDisplayScope(route);
130
+ const originatingTo = providedOriginatingTo || displayTo;
80
131
  const body = command.body;
81
132
  if (!clientId) {
82
- emitBncrLogLine('warn', '[bncr] inbound missing clientId for native command identity');
83
- return { handled: false };
133
+ emitBncrLogLine(
134
+ 'warn',
135
+ '[bncr] inbound missing clientId for native command identity; using route identity fallback',
136
+ );
84
137
  }
85
- const senderIdForContext = clientId;
86
- const senderDisplayName = 'bncr-client';
138
+ const senderIdForContext = clientId || displayTo;
139
+ const senderDisplayName = clientId ? 'bncr-client' : displayTo;
87
140
  const storePath = api.runtime.channel.session.resolveStorePath(cfg?.session?.store, {
88
141
  agentId: resolvedRoute.agentId,
89
142
  });
90
143
 
91
- const ctxPayload = api.runtime.channel.reply.finalizeInboundContext({
92
- Body: body,
93
- BodyForAgent: body,
94
- RawBody: body,
95
- CommandBody: body,
96
- BodyForCommands: body,
97
- From: senderIdForContext,
98
- To: displayTo,
99
- SessionKey: sessionKey,
100
- CommandTargetSessionKey: sessionKey,
101
- CommandSource: 'native',
102
- CommandAuthorized: true,
103
- AccountId: accountId,
104
- ChatType: peer.kind,
105
- ConversationLabel: displayTo,
106
- SenderId: senderIdForContext,
107
- SenderName: senderDisplayName,
108
- SenderUsername: senderDisplayName,
109
- Provider: channelId,
110
- Surface: channelId,
111
- WasMentioned: true,
112
- MessageSid: msgId,
113
- Timestamp: Date.now(),
114
- OriginatingChannel: channelId,
115
- OriginatingTo: displayTo,
144
+ const ctxPayload = api.runtime.channel.turn.buildContext({
145
+ channel: channelId,
146
+ provider: channelId,
147
+ surface: channelId,
148
+ accountId,
149
+ messageId: msgId,
150
+ timestamp: Date.now(),
151
+ from: senderIdForContext,
152
+ sender: {
153
+ id: senderIdForContext,
154
+ name: senderDisplayName,
155
+ username: senderDisplayName,
156
+ },
157
+ conversation: {
158
+ kind: peer.kind,
159
+ id: peer.id,
160
+ label: displayTo,
161
+ routePeer: {
162
+ kind: peer.kind,
163
+ id: peer.id,
164
+ },
165
+ },
166
+ route: {
167
+ agentId: resolvedRoute.agentId,
168
+ accountId,
169
+ routeSessionKey: resolvedRoute.sessionKey,
170
+ dispatchSessionKey: sessionKey,
171
+ mainSessionKey: resolvedRoute.mainSessionKey,
172
+ },
173
+ reply: {
174
+ to: displayTo,
175
+ originatingTo,
176
+ replyToId: msgId,
177
+ },
178
+ message: {
179
+ inboundEventKind: 'user_request',
180
+ body,
181
+ rawBody: body,
182
+ bodyForAgent: body,
183
+ commandBody: body,
184
+ envelopeFrom: originatingTo,
185
+ senderLabel: senderDisplayName,
186
+ },
187
+ commandTurn: {
188
+ kind: 'native',
189
+ source: 'native',
190
+ authorized: true,
191
+ body,
192
+ },
193
+ access: {
194
+ mentions: {
195
+ canDetectMention: true,
196
+ wasMentioned: true,
197
+ effectiveWasMentioned: true,
198
+ },
199
+ commands: {
200
+ authorized: true,
201
+ allowTextCommands: true,
202
+ useAccessGroups: false,
203
+ authorizers: [],
204
+ },
205
+ },
206
+ extra: {
207
+ OriginatingChannel: channelId,
208
+ },
209
+ });
210
+
211
+ const sessionIdentityPatch = buildBncrInboundSessionIdentityPatch({
212
+ channelId,
213
+ accountId,
214
+ chatType: peer.kind,
215
+ displayTo,
216
+ senderId: senderIdForContext,
116
217
  });
117
218
 
118
- await api.runtime.channel.session.recordInboundSession({
219
+ const nativeVerbose = resolveBncrNativeVerboseCommand(command);
220
+ if (nativeVerbose) {
221
+ logBncrNativeCommandEvent('handled-verbose', {
222
+ command: command.command,
223
+ accountId,
224
+ sessionKey,
225
+ to: displayTo,
226
+ msgId: msgId || null,
227
+ fallbackToAgent: false,
228
+ });
229
+ await recordAndPatchBncrInboundSessionEntry({
230
+ storePath,
231
+ sessionKey,
232
+ ctx: ctxPayload,
233
+ patch: {
234
+ ...sessionIdentityPatch,
235
+ ...(nativeVerbose.verboseLevel ? { verboseLevel: nativeVerbose.verboseLevel } : {}),
236
+ },
237
+ });
238
+ rememberSessionRoute(baseSessionKey, accountId, route);
239
+ await enqueueFromReply({
240
+ accountId,
241
+ sessionKey,
242
+ route,
243
+ payload: {
244
+ text: nativeVerbose.text,
245
+ replyToId: msgId || undefined,
246
+ },
247
+ });
248
+ return { handled: true, command: command.command, sessionKey };
249
+ }
250
+
251
+ await recordAndPatchBncrInboundSessionEntry({
119
252
  storePath,
120
253
  sessionKey,
121
254
  ctx: ctxPayload,
122
- onRecordError: (err: unknown) => {
123
- emitBncrLogLine(
124
- 'warn',
125
- `[bncr] inbound record native command session failed: ${String(err)}`,
126
- );
127
- },
255
+ patch: sessionIdentityPatch,
128
256
  });
129
257
 
130
258
  const effectiveReply = buildBncrReplyConfig(cfg);
259
+ const channelPolicy = resolveBncrChannelPolicy(cfg?.channels?.bncr || {});
260
+ const pinnedMainDmOwner =
261
+ peer.kind === 'direct'
262
+ ? resolvePinnedMainDmOwnerFromAllowlist({
263
+ dmScope: cfg?.session?.dmScope,
264
+ allowFrom: channelPolicy.allowFrom,
265
+ normalizeEntry: (entry: string) => String(entry || '').trim(),
266
+ })
267
+ : null;
268
+ const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
269
+ route: resolvedRoute,
270
+ sessionKey,
271
+ });
131
272
 
132
273
  let responded = false;
133
- await api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
134
- ctx: ctxPayload,
135
- cfg: effectiveReply.replyCfg,
136
- dispatcherOptions: {
137
- deliver: async (
138
- payload: { text?: string; mediaUrl?: string; mediaUrls?: string[]; audioAsVoice?: boolean },
139
- info?: { kind?: 'tool' | 'block' | 'final' },
140
- ) => {
141
- const kind = info?.kind;
142
- const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
143
-
144
- if (kind === 'tool' && !shouldForwardTool) {
145
- return;
146
- }
147
-
148
- const hasPayload = Boolean(
149
- payload?.text ||
150
- payload?.mediaUrl ||
151
- (Array.isArray(payload?.mediaUrls) && payload.mediaUrls.length > 0),
152
- );
153
- if (!hasPayload) return;
154
- responded = true;
155
- await enqueueFromReply({
156
- accountId,
157
- sessionKey,
158
- route,
159
- payload: {
160
- ...payload,
161
- kind: kind as 'tool' | 'block' | 'final' | undefined,
162
- replyToId: msgId || undefined,
163
- },
164
- });
165
- },
274
+ logBncrNativeCommandEvent(
275
+ 'dispatch-native-turn',
276
+ {
277
+ command: command.command,
278
+ accountId,
279
+ sessionKey,
280
+ to: displayTo,
281
+ msgId: msgId || null,
166
282
  },
167
- replyOptions: {
168
- disableBlockStreaming: !effectiveReply.blockStreaming,
169
- shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
283
+ { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
284
+ );
285
+ await api.runtime.channel.turn.run({
286
+ channel: channelId,
287
+ accountId,
288
+ raw: parsed,
289
+ adapter: {
290
+ ingest: () => ({
291
+ id: msgId ?? `${displayTo}:${Date.now()}`,
292
+ timestamp: Date.now(),
293
+ rawText: body,
294
+ textForAgent: ctxPayload.BodyForAgent,
295
+ textForCommands: ctxPayload.CommandBody,
296
+ raw: parsed,
297
+ }),
298
+ resolveTurn: () => ({
299
+ channel: channelId,
300
+ accountId,
301
+ routeSessionKey: resolvedRoute.sessionKey,
302
+ storePath,
303
+ ctxPayload,
304
+ recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
305
+ recordInboundSession: api.runtime.channel.session.recordInboundSession,
306
+ expectedLabel: displayTo,
307
+ }),
308
+ record: {
309
+ updateLastRoute:
310
+ peer.kind === 'direct'
311
+ ? {
312
+ sessionKey: inboundLastRouteSessionKey,
313
+ channel: channelId,
314
+ to: displayTo,
315
+ accountId,
316
+ mainDmOwnerPin:
317
+ inboundLastRouteSessionKey === resolvedRoute.mainSessionKey && pinnedMainDmOwner
318
+ ? {
319
+ ownerRecipient: pinnedMainDmOwner,
320
+ senderRecipient: senderIdForContext,
321
+ }
322
+ : undefined,
323
+ }
324
+ : undefined,
325
+ onRecordError: (err: unknown) => {
326
+ emitBncrLogLine(
327
+ 'warn',
328
+ `[bncr] inbound record native command session failed: ${String(err)}`,
329
+ );
330
+ },
331
+ },
332
+ runDispatch: () =>
333
+ api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
334
+ ctx: ctxPayload,
335
+ cfg: effectiveReply.replyCfg,
336
+ dispatcherOptions: {
337
+ deliver: async (
338
+ payload: {
339
+ text?: string;
340
+ mediaUrl?: string;
341
+ mediaUrls?: string[];
342
+ audioAsVoice?: boolean;
343
+ },
344
+ info?: { kind?: 'tool' | 'block' | 'final' },
345
+ ) => {
346
+ const kind = info?.kind;
347
+ const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
348
+
349
+ if (kind === 'tool' && !shouldForwardTool) {
350
+ return;
351
+ }
352
+
353
+ const hasPayload = Boolean(
354
+ payload?.text ||
355
+ payload?.mediaUrl ||
356
+ (Array.isArray(payload?.mediaUrls) && payload.mediaUrls.length > 0),
357
+ );
358
+ if (!hasPayload) return;
359
+ if (!responded) {
360
+ logBncrNativeCommandEvent(
361
+ 'payload-produced',
362
+ {
363
+ command: command.command,
364
+ accountId,
365
+ sessionKey,
366
+ to: displayTo,
367
+ msgId: msgId || null,
368
+ kind: kind || null,
369
+ fallbackToAgent: false,
370
+ },
371
+ { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
372
+ );
373
+ }
374
+ responded = true;
375
+ await enqueueFromReply({
376
+ accountId,
377
+ sessionKey,
378
+ route,
379
+ payload: {
380
+ ...payload,
381
+ kind: kind as 'tool' | 'block' | 'final' | undefined,
382
+ replyToId: msgId || undefined,
383
+ },
384
+ });
385
+ },
386
+ },
387
+ replyOptions: {
388
+ disableBlockStreaming: !effectiveReply.blockStreaming,
389
+ shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
390
+ },
391
+ }),
392
+ }),
170
393
  },
171
394
  });
172
395
 
173
396
  if (!responded) {
174
- return { handled: false };
397
+ logBncrNativeCommandEvent('no-payload-fallback-to-agent', {
398
+ command: command.command,
399
+ accountId,
400
+ sessionKey,
401
+ to: displayTo,
402
+ msgId: msgId || null,
403
+ fallbackToAgent: true,
404
+ });
405
+ return { handled: true, command: command.command, sessionKey, fallbackToAgent: true };
175
406
  }
176
407
 
408
+ logBncrNativeCommandEvent(
409
+ 'handled-with-payload',
410
+ {
411
+ command: command.command,
412
+ accountId,
413
+ sessionKey,
414
+ to: displayTo,
415
+ msgId: msgId || null,
416
+ fallbackToAgent: false,
417
+ },
418
+ { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
419
+ );
177
420
  return { handled: true, command: command.command, sessionKey };
178
421
  }