@xmoxmo/bncr 0.2.5 → 0.2.7

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 +9 -3
  2. package/index.ts +30 -15
  3. package/package.json +4 -3
  4. package/scripts/check-pack.mjs +61 -0
  5. package/scripts/selfcheck.mjs +10 -0
  6. package/src/channel.ts +892 -255
  7. package/src/core/connection-reachability.ts +41 -14
  8. package/src/core/diagnostics.ts +7 -2
  9. package/src/core/downlink-health.ts +7 -2
  10. package/src/core/outbox-entry-builders.ts +3 -2
  11. package/src/core/policy.ts +9 -0
  12. package/src/core/register-trace.ts +6 -1
  13. package/src/core/status.ts +7 -2
  14. package/src/core/targets.ts +10 -1
  15. package/src/core/types.ts +1 -0
  16. package/src/messaging/inbound/commands.ts +330 -77
  17. package/src/messaging/inbound/context-facts.ts +200 -0
  18. package/src/messaging/inbound/dispatch.ts +429 -119
  19. package/src/messaging/inbound/gate.ts +66 -26
  20. package/src/messaging/inbound/parse.ts +8 -0
  21. package/src/messaging/inbound/runtime-compat.ts +39 -0
  22. package/src/messaging/inbound/session-label.ts +115 -0
  23. package/src/messaging/outbound/diagnostics.ts +16 -0
  24. package/src/messaging/outbound/durable-message-adapter.ts +107 -0
  25. package/src/messaging/outbound/durable-queue-adapter.ts +157 -0
  26. package/src/messaging/outbound/media.ts +3 -1
  27. package/src/messaging/outbound/queue-selectors.ts +7 -2
  28. package/src/messaging/outbound/reasons.ts +4 -0
  29. package/src/messaging/outbound/reply-enqueue.ts +2 -2
  30. package/src/messaging/outbound/reply-target-policy.ts +13 -0
  31. package/src/messaging/outbound/retry-policy.ts +12 -3
  32. package/src/messaging/outbound/send.ts +6 -0
  33. package/src/messaging/outbound/session-route.ts +2 -2
  34. package/src/openclaw/config-runtime.ts +52 -0
  35. package/src/openclaw/inbound-session-runtime.ts +94 -0
  36. package/src/openclaw/ingress-runtime.ts +35 -0
  37. package/src/openclaw/media-runtime.ts +73 -0
  38. package/src/openclaw/reply-runtime.ts +104 -0
  39. package/src/openclaw/routing-runtime.ts +48 -0
  40. package/src/openclaw/sdk-helpers.ts +20 -0
  41. package/src/openclaw/session-route-runtime.ts +15 -0
@@ -1,29 +1,156 @@
1
+ import fs from 'node:fs';
1
2
  import { emitBncrLogLine } from '../../core/logging.ts';
3
+ import { resolveBncrChannelPolicy } from '../../core/policy.ts';
2
4
  import {
3
5
  formatDisplayScope,
4
6
  normalizeInboundSessionKey,
5
7
  withTaskSessionKey,
6
8
  } from '../../core/targets.ts';
7
9
  import { handleBncrNativeCommand } from './commands.ts';
10
+ import {
11
+ buildBncrPromptVisibleContextFacts,
12
+ buildBncrStructuredContextFactsFromInboundParts,
13
+ } from './context-facts.ts';
8
14
  import { buildBncrReplyConfig } from './reply-config.ts';
15
+ import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
16
+ import { wrapBncrInboundRecordSessionLabelCorrection } from './session-label.ts';
17
+ import { saveOpenClawChannelMediaBuffer } from '../../openclaw/media-runtime.ts';
18
+ import {
19
+ dispatchOpenClawReplyWithBufferedBlockDispatcher,
20
+ formatOpenClawAgentEnvelope,
21
+ resolveOpenClawEnvelopeFormatOptions,
22
+ } from '../../openclaw/reply-runtime.ts';
23
+ import {
24
+ resolveOpenClawAgentRoute,
25
+ resolveOpenClawInboundLastRouteSessionKey,
26
+ } from '../../openclaw/routing-runtime.ts';
27
+ import {
28
+ readBncrSessionUpdatedAt,
29
+ recordBncrInboundSession,
30
+ resolveBncrInboundSessionStorePath,
31
+ resolveBncrPinnedMainDmOwnerFromAllowlist,
32
+ } from '../../openclaw/inbound-session-runtime.ts';
9
33
 
10
34
  type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
11
35
 
12
- async function prepareBncrInboundSessionContext(args: {
36
+ type BncrInboundConversationResolution = {
37
+ accountId: string;
38
+ chatType: 'direct' | 'group';
39
+ route: ParsedInbound['route'];
40
+ resolvedRoute: {
41
+ sessionKey: string;
42
+ agentId: string;
43
+ mainSessionKey?: string;
44
+ };
45
+ canonicalTo: string;
46
+ rawTo: string;
47
+ originatingTo: string;
48
+ baseSessionKey: string;
49
+ taskSessionKey?: string;
50
+ dispatchSessionKey: string;
51
+ };
52
+
53
+ type BncrInboundReplyRouteFact = {
54
+ accountId: string;
55
+ sessionKey: string;
56
+ route: ParsedInbound['route'];
57
+ canonicalTo: string;
58
+ originatingTo: string;
59
+ chatType: 'direct' | 'group';
60
+ };
61
+
62
+ const INBOUND_MEDIA_MAX_BYTES = 30 * 1024 * 1024;
63
+
64
+ export function estimateBase64DecodedBytes(value: string): number {
65
+ const normalized = String(value || '').replace(/\s+/g, '');
66
+ if (!normalized) return 0;
67
+ const padding = normalized.endsWith('==') ? 2 : normalized.endsWith('=') ? 1 : 0;
68
+ return Math.max(0, Math.floor((normalized.length * 3) / 4) - padding);
69
+ }
70
+
71
+ export function assertInboundMediaBase64Size(
72
+ value: string,
73
+ maxBytes = INBOUND_MEDIA_MAX_BYTES,
74
+ ) {
75
+ const estimatedBytes = estimateBase64DecodedBytes(value);
76
+ if (estimatedBytes > maxBytes) {
77
+ throw new Error(
78
+ `inbound media too large: estimated ${estimatedBytes} bytes exceeds ${maxBytes} bytes`,
79
+ );
80
+ }
81
+ }
82
+
83
+ export function decodeInboundMediaBase64(
84
+ value: string,
85
+ maxBytes = INBOUND_MEDIA_MAX_BYTES,
86
+ ): Buffer {
87
+ assertInboundMediaBase64Size(value, maxBytes);
88
+ const normalized = String(value || '').replace(/\s+/g, '');
89
+ const mediaBuf = Buffer.from(normalized, 'base64');
90
+ if (!mediaBuf.length) {
91
+ throw new Error('inbound media base64 decoded to empty buffer');
92
+ }
93
+ if (mediaBuf.length > maxBytes) {
94
+ throw new Error(
95
+ `inbound media too large: decoded ${mediaBuf.length} bytes exceeds ${maxBytes} bytes`,
96
+ );
97
+ }
98
+ return mediaBuf;
99
+ }
100
+
101
+ function formatRawBncrInboundTarget(route: ParsedInbound['route']): string {
102
+ return `Bncr:${String(route.platform || '').trim()}:${String(route.groupId || '').trim()}:${String(route.userId || '').trim()}`;
103
+ }
104
+
105
+ export function resolveBncrInboundConversation(args: {
13
106
  api: any;
14
107
  cfg: any;
15
108
  channelId: string;
16
109
  parsed: ParsedInbound;
17
110
  canonicalAgentId: string;
18
- rememberSessionRoute: (sessionKey: string, accountId: string, route: any) => void;
19
111
  }) {
20
- const { api, cfg, channelId, parsed, canonicalAgentId, rememberSessionRoute } = args;
21
- const {
112
+ const { api, cfg, channelId, parsed, canonicalAgentId } = args;
113
+ const { accountId, route, peer, sessionKeyfromroute, providedOriginatingTo, extracted } = parsed;
114
+
115
+ const resolvedRoute = resolveOpenClawAgentRoute(api, {
116
+ cfg,
117
+ channel: channelId,
22
118
  accountId,
23
- route,
24
119
  peer,
25
- sessionKeyfromroute,
26
- text,
120
+ });
121
+
122
+ const baseSessionKey =
123
+ normalizeInboundSessionKey(sessionKeyfromroute, route, canonicalAgentId) ||
124
+ resolvedRoute.sessionKey;
125
+ const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
126
+ const dispatchSessionKey = taskSessionKey || baseSessionKey;
127
+ const rawTo = formatRawBncrInboundTarget(route);
128
+ const canonicalTo = formatDisplayScope(route);
129
+ const originatingTo = providedOriginatingTo || rawTo;
130
+
131
+ return {
132
+ accountId,
133
+ chatType: peer.kind,
134
+ route,
135
+ resolvedRoute,
136
+ canonicalTo,
137
+ rawTo,
138
+ originatingTo,
139
+ baseSessionKey,
140
+ ...(taskSessionKey ? { taskSessionKey } : {}),
141
+ dispatchSessionKey,
142
+ } satisfies BncrInboundConversationResolution;
143
+ }
144
+
145
+ async function prepareBncrInboundSessionContext(args: {
146
+ api: any;
147
+ cfg: any;
148
+ parsed: ParsedInbound;
149
+ resolution: BncrInboundConversationResolution;
150
+ rememberSessionRoute: (sessionKey: string, accountId: string, route: any) => void;
151
+ }) {
152
+ const { api, cfg, parsed, resolution, rememberSessionRoute } = args;
153
+ const {
27
154
  msgType,
28
155
  mediaBase64,
29
156
  mediaPathFromTransfer,
@@ -34,34 +161,23 @@ async function prepareBncrInboundSessionContext(args: {
34
161
  groupId,
35
162
  userId,
36
163
  } = parsed;
37
-
38
- const resolvedRoute = api.runtime.channel.routing.resolveAgentRoute({
39
- cfg,
40
- channel: channelId,
41
- accountId,
42
- peer,
43
- });
44
-
45
- const baseSessionKey =
46
- normalizeInboundSessionKey(sessionKeyfromroute, route, canonicalAgentId) ||
47
- resolvedRoute.sessionKey;
48
- const agentText = extracted.text;
49
- const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
50
- const sessionKey = taskSessionKey || baseSessionKey;
164
+ const { accountId, route, resolvedRoute, baseSessionKey, taskSessionKey, dispatchSessionKey } = resolution;
51
165
 
52
166
  rememberSessionRoute(baseSessionKey, accountId, route);
53
167
  if (taskSessionKey && taskSessionKey !== baseSessionKey) {
54
168
  rememberSessionRoute(taskSessionKey, accountId, route);
55
169
  }
56
170
 
57
- const storePath = api.runtime.channel.session.resolveStorePath(cfg?.session?.store, {
171
+ const storePath = resolveBncrInboundSessionStorePath({
172
+ storeConfig: cfg?.session?.store,
58
173
  agentId: resolvedRoute.agentId,
59
174
  });
60
175
 
61
176
  let mediaPath: string | undefined;
62
177
  if (mediaBase64) {
63
- const mediaBuf = Buffer.from(mediaBase64, 'base64');
64
- const saved = await api.runtime.channel.media.saveMediaBuffer(
178
+ const mediaBuf = decodeInboundMediaBase64(mediaBase64);
179
+ const saved = await saveOpenClawChannelMediaBuffer(
180
+ api,
65
181
  mediaBuf,
66
182
  mimeType,
67
183
  'inbound',
@@ -73,30 +189,189 @@ async function prepareBncrInboundSessionContext(args: {
73
189
  mediaPath = mediaPathFromTransfer;
74
190
  }
75
191
 
76
- const rawBody = agentText || (msgType === 'text' ? '' : `[${msgType}]`);
77
- const body = api.runtime.channel.reply.formatAgentEnvelope({
192
+ const rawBody = extracted.text || (msgType === 'text' ? '' : `[${msgType}]`);
193
+ const body = formatOpenClawAgentEnvelope(api, {
78
194
  channel: 'Bncr',
79
195
  from: `${platform}:${groupId}:${userId}`,
80
196
  timestamp: Date.now(),
81
- previousTimestamp: api.runtime.channel.session.readSessionUpdatedAt({
197
+ previousTimestamp: readBncrSessionUpdatedAt(api, {
82
198
  storePath,
83
- sessionKey,
199
+ sessionKey: dispatchSessionKey,
84
200
  }),
85
- envelope: api.runtime.channel.reply.resolveEnvelopeFormatOptions(cfg),
201
+ envelope: resolveOpenClawEnvelopeFormatOptions(api, cfg),
86
202
  body: rawBody,
87
203
  });
88
204
 
89
- const displayTo = formatDisplayScope(route);
90
205
  return {
91
- resolvedRoute,
92
- baseSessionKey,
93
- taskSessionKey,
94
- sessionKey,
95
206
  storePath,
96
207
  mediaPath,
97
208
  rawBody,
98
209
  body,
99
- displayTo,
210
+ };
211
+ }
212
+
213
+ function buildBncrInboundTurnContext(args: {
214
+ api: any;
215
+ channelId: string;
216
+ parsed: ParsedInbound;
217
+ msgId?: string | null;
218
+ mimeType?: string;
219
+ mediaPath?: string;
220
+ peer: ParsedInbound['peer'];
221
+ senderIdForContext: string;
222
+ senderDisplayName: string;
223
+ resolution: BncrInboundConversationResolution;
224
+ prepared: {
225
+ rawBody: string;
226
+ body: string;
227
+ };
228
+ }) {
229
+ const {
230
+ api,
231
+ channelId,
232
+ parsed,
233
+ msgId,
234
+ mimeType,
235
+ mediaPath,
236
+ peer,
237
+ senderIdForContext,
238
+ senderDisplayName,
239
+ resolution,
240
+ prepared,
241
+ } = args;
242
+ const structuredContextFacts = buildBncrStructuredContextFactsFromInboundParts({
243
+ channelId,
244
+ parsed,
245
+ resolution,
246
+ prepared: {
247
+ rawBody: prepared.rawBody,
248
+ body: prepared.body,
249
+ mediaPath,
250
+ },
251
+ senderIdForContext,
252
+ senderDisplayName,
253
+ });
254
+ const promptVisibleContextFacts = buildBncrPromptVisibleContextFacts(structuredContextFacts);
255
+ const supplementalUntrustedContext = Object.keys(promptVisibleContextFacts).length
256
+ ? [
257
+ {
258
+ label: 'Bncr inbound context',
259
+ source: channelId,
260
+ type: 'bncr.inbound_context',
261
+ payload: promptVisibleContextFacts,
262
+ },
263
+ ]
264
+ : [];
265
+
266
+ return resolveBncrChannelInboundRuntime(api).buildContext({
267
+ channel: channelId,
268
+ provider: channelId,
269
+ surface: channelId,
270
+ accountId: resolution.accountId,
271
+ messageId: msgId,
272
+ timestamp: Date.now(),
273
+ from: senderIdForContext,
274
+ sender: {
275
+ id: senderIdForContext,
276
+ name: senderDisplayName,
277
+ username: senderDisplayName,
278
+ },
279
+ conversation: {
280
+ kind: resolution.chatType,
281
+ id: peer.id,
282
+ label: resolution.canonicalTo,
283
+ routePeer: {
284
+ kind: peer.kind,
285
+ id: peer.id,
286
+ },
287
+ },
288
+ route: {
289
+ agentId: resolution.resolvedRoute.agentId,
290
+ accountId: resolution.accountId,
291
+ routeSessionKey: resolution.resolvedRoute.sessionKey,
292
+ dispatchSessionKey: resolution.dispatchSessionKey,
293
+ mainSessionKey: resolution.resolvedRoute.mainSessionKey,
294
+ },
295
+ reply: {
296
+ to: resolution.canonicalTo,
297
+ originatingTo: resolution.originatingTo,
298
+ },
299
+ message: {
300
+ inboundEventKind: 'user_request',
301
+ body: prepared.body,
302
+ rawBody: prepared.rawBody,
303
+ bodyForAgent: prepared.rawBody,
304
+ commandBody: prepared.rawBody,
305
+ envelopeFrom: resolution.originatingTo,
306
+ senderLabel: senderDisplayName,
307
+ },
308
+ media: mediaPath
309
+ ? [
310
+ {
311
+ path: mediaPath,
312
+ contentType: mimeType,
313
+ kind: mimeType?.startsWith('image/')
314
+ ? 'image'
315
+ : mimeType?.startsWith('video/')
316
+ ? 'video'
317
+ : mimeType?.startsWith('audio/')
318
+ ? 'audio'
319
+ : 'document',
320
+ messageId: msgId ?? undefined,
321
+ },
322
+ ]
323
+ : [],
324
+ supplemental: {
325
+ untrustedContext: supplementalUntrustedContext,
326
+ },
327
+ extra: {
328
+ OriginatingChannel: channelId,
329
+ BncrStructuredContextFacts: structuredContextFacts,
330
+ StructuredContextFacts: structuredContextFacts,
331
+ },
332
+ });
333
+ }
334
+
335
+ function buildBncrInboundRecordUpdateLastRoute(args: {
336
+ channelId: string;
337
+ peer: ParsedInbound['peer'];
338
+ senderIdForContext: string;
339
+ resolution: BncrInboundConversationResolution;
340
+ pinnedMainDmOwner: string | null;
341
+ }) {
342
+ const { channelId, peer, senderIdForContext, resolution, pinnedMainDmOwner } = args;
343
+ if (peer.kind !== 'direct') return undefined;
344
+
345
+ const sessionKey = resolveOpenClawInboundLastRouteSessionKey({
346
+ route: resolution.resolvedRoute,
347
+ sessionKey: resolution.dispatchSessionKey,
348
+ });
349
+
350
+ return {
351
+ sessionKey,
352
+ channel: channelId,
353
+ to: resolution.canonicalTo,
354
+ accountId: resolution.accountId,
355
+ mainDmOwnerPin:
356
+ sessionKey === resolution.resolvedRoute.mainSessionKey && pinnedMainDmOwner
357
+ ? {
358
+ ownerRecipient: pinnedMainDmOwner,
359
+ senderRecipient: senderIdForContext,
360
+ }
361
+ : undefined,
362
+ };
363
+ }
364
+
365
+ function buildBncrInboundReplyRouteFact(
366
+ resolution: BncrInboundConversationResolution,
367
+ ): BncrInboundReplyRouteFact {
368
+ return {
369
+ accountId: resolution.accountId,
370
+ sessionKey: resolution.dispatchSessionKey,
371
+ route: resolution.route,
372
+ canonicalTo: resolution.canonicalTo,
373
+ originatingTo: resolution.originatingTo,
374
+ chatType: resolution.chatType,
100
375
  };
101
376
  }
102
377
 
@@ -130,7 +405,7 @@ export async function dispatchBncrInbound(params: {
130
405
  scheduleSave,
131
406
  logger,
132
407
  } = params;
133
- const { accountId, route, clientId, msgId, extracted, mimeType, peer } = parsed;
408
+ const { accountId, clientId, msgId, extracted, mimeType, peer } = parsed;
134
409
 
135
410
  const nativeCommand = await handleBncrNativeCommand({
136
411
  api,
@@ -142,7 +417,7 @@ export async function dispatchBncrInbound(params: {
142
417
  enqueueFromReply,
143
418
  logger,
144
419
  });
145
- if (nativeCommand.handled) {
420
+ if (nativeCommand.handled && !nativeCommand.fallbackToAgent) {
146
421
  const inboundAt = Date.now();
147
422
  setInboundActivity(accountId, inboundAt);
148
423
  scheduleSave();
@@ -154,104 +429,139 @@ export async function dispatchBncrInbound(params: {
154
429
  };
155
430
  }
156
431
 
157
- const {
158
- resolvedRoute,
159
- sessionKey,
160
- storePath,
161
- mediaPath,
162
- rawBody,
163
- body,
164
- displayTo,
165
- } = await prepareBncrInboundSessionContext({
432
+ const resolution = resolveBncrInboundConversation({
166
433
  api,
167
434
  cfg,
168
435
  channelId,
169
436
  parsed,
170
437
  canonicalAgentId,
438
+ });
439
+ const { resolvedRoute, canonicalTo, dispatchSessionKey: sessionKey } = resolution;
440
+ const prepared = await prepareBncrInboundSessionContext({
441
+ api,
442
+ cfg,
443
+ parsed,
444
+ resolution,
171
445
  rememberSessionRoute,
172
446
  });
447
+ const { storePath, mediaPath, rawBody, body } = prepared;
448
+ const replyRouteFact = buildBncrInboundReplyRouteFact(resolution);
173
449
  if (!clientId) {
174
- emitBncrLogLine('warn', '[bncr] inbound missing clientId for chat identity');
175
- return {
176
- accountId,
177
- sessionKey,
178
- taskKey: extracted.taskKey ?? null,
179
- msgId: msgId ?? null,
180
- };
450
+ emitBncrLogLine(
451
+ 'warn',
452
+ '[bncr] inbound missing clientId for chat identity; using route identity fallback',
453
+ );
181
454
  }
182
- const senderIdForContext = clientId;
183
- const senderDisplayName = 'bncr-client';
184
- const ctxPayload = api.runtime.channel.reply.finalizeInboundContext({
185
- Body: body,
186
- BodyForAgent: rawBody,
187
- RawBody: rawBody,
188
- CommandBody: rawBody,
189
- MediaPath: mediaPath,
190
- MediaType: mimeType,
191
- From: senderIdForContext,
192
- To: displayTo,
193
- SessionKey: sessionKey,
194
- AccountId: accountId,
195
- ChatType: peer.kind,
196
- ConversationLabel: displayTo,
197
- SenderId: senderIdForContext,
198
- SenderName: senderDisplayName,
199
- SenderUsername: senderDisplayName,
200
- Provider: channelId,
201
- Surface: channelId,
202
- MessageSid: msgId,
203
- Timestamp: Date.now(),
204
- OriginatingChannel: channelId,
205
- OriginatingTo: displayTo,
455
+ const senderIdForContext = clientId || canonicalTo;
456
+ const senderDisplayName = clientId ? 'bncr-client' : canonicalTo;
457
+ const ctxPayload = buildBncrInboundTurnContext({
458
+ api,
459
+ channelId,
460
+ parsed,
461
+ msgId,
462
+ mimeType,
463
+ mediaPath,
464
+ peer,
465
+ senderIdForContext,
466
+ senderDisplayName,
467
+ resolution,
468
+ prepared,
206
469
  });
207
470
 
208
- await api.runtime.channel.session.recordInboundSession({
209
- storePath,
210
- sessionKey,
211
- ctx: ctxPayload,
212
- onRecordError: (err: unknown) => {
213
- emitBncrLogLine('warn', `[bncr] inbound record session failed: ${String(err)}`);
214
- },
471
+ const effectiveReply = buildBncrReplyConfig(cfg);
472
+ const channelPolicy = resolveBncrChannelPolicy(cfg?.channels?.bncr || {});
473
+ const pinnedMainDmOwner =
474
+ peer.kind === 'direct'
475
+ ? resolveBncrPinnedMainDmOwnerFromAllowlist({
476
+ dmScope: cfg?.session?.dmScope,
477
+ allowFrom: channelPolicy.allowFrom,
478
+ normalizeEntry: (entry: string) => String(entry || '').trim(),
479
+ })
480
+ : null;
481
+ const updateLastRoute = buildBncrInboundRecordUpdateLastRoute({
482
+ channelId,
483
+ peer,
484
+ senderIdForContext,
485
+ resolution,
486
+ pinnedMainDmOwner,
215
487
  });
216
488
 
217
- const inboundAt = Date.now();
218
- setInboundActivity(accountId, inboundAt);
219
- scheduleSave();
489
+ await resolveBncrChannelInboundRuntime(api).run({
490
+ channel: channelId,
491
+ accountId,
492
+ raw: parsed,
493
+ adapter: {
494
+ ingest: () => ({
495
+ id: msgId ?? `${canonicalTo}:${Date.now()}`,
496
+ timestamp: Date.now(),
497
+ rawText: rawBody,
498
+ textForAgent: ctxPayload.BodyForAgent,
499
+ textForCommands: ctxPayload.CommandBody,
500
+ raw: parsed,
501
+ }),
502
+ resolveTurn: () => ({
503
+ channel: channelId,
504
+ accountId,
505
+ routeSessionKey: resolvedRoute.sessionKey,
506
+ storePath,
507
+ ctxPayload,
508
+ recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
509
+ recordInboundSession: recordBncrInboundSession,
510
+ expectedLabel: canonicalTo,
511
+ }),
512
+ record: {
513
+ updateLastRoute,
514
+ onRecordError: (err: unknown) => {
515
+ emitBncrLogLine('warn', `[bncr] inbound record session failed: ${String(err)}`);
516
+ },
517
+ },
518
+ runDispatch: () =>
519
+ dispatchOpenClawReplyWithBufferedBlockDispatcher(api, {
520
+ ctx: ctxPayload,
521
+ cfg: effectiveReply.replyCfg,
522
+ dispatcherOptions: {
523
+ deliver: async (
524
+ payload: {
525
+ text?: string;
526
+ mediaUrl?: string;
527
+ mediaUrls?: string[];
528
+ audioAsVoice?: boolean;
529
+ },
530
+ info?: { kind?: 'tool' | 'block' | 'final' },
531
+ ) => {
532
+ const kind = info?.kind;
533
+ const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
220
534
 
221
- const effectiveReply = buildBncrReplyConfig(cfg);
535
+ if (kind === 'tool' && !shouldForwardTool) {
536
+ return;
537
+ }
222
538
 
223
- await api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
224
- ctx: ctxPayload,
225
- cfg: effectiveReply.replyCfg,
226
- dispatcherOptions: {
227
- deliver: async (
228
- payload: { text?: string; mediaUrl?: string; mediaUrls?: string[]; audioAsVoice?: boolean },
229
- info?: { kind?: 'tool' | 'block' | 'final' },
230
- ) => {
231
- const kind = info?.kind;
232
- const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
233
-
234
- if (kind === 'tool' && !shouldForwardTool) {
235
- return;
236
- }
237
-
238
- await enqueueFromReply({
239
- accountId,
240
- sessionKey,
241
- route,
242
- payload: {
243
- ...payload,
244
- kind: kind as 'tool' | 'block' | 'final' | undefined,
245
- },
246
- });
539
+ await enqueueFromReply({
540
+ accountId: replyRouteFact.accountId,
541
+ sessionKey: replyRouteFact.sessionKey,
542
+ route: replyRouteFact.route,
543
+ payload: {
544
+ ...payload,
545
+ kind: kind as 'tool' | 'block' | 'final' | undefined,
546
+ replyToId: msgId || undefined,
547
+ },
548
+ });
549
+ },
550
+ onError: (err: unknown) => {
551
+ emitBncrLogLine('error', `[bncr] outbound reply failed: ${String(err)}`);
552
+ },
553
+ },
554
+ replyOptions: {
555
+ disableBlockStreaming: !effectiveReply.blockStreaming,
556
+ shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
557
+ },
558
+ }),
559
+ }),
560
+ onFinalize: () => {
561
+ const inboundAt = Date.now();
562
+ setInboundActivity(accountId, inboundAt);
563
+ scheduleSave();
247
564
  },
248
- onError: (err: unknown) => {
249
- emitBncrLogLine('error', `[bncr] outbound reply failed: ${String(err)}`);
250
- },
251
- },
252
- replyOptions: {
253
- disableBlockStreaming: !effectiveReply.blockStreaming,
254
- shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
255
565
  },
256
566
  });
257
567