@xmoxmo/bncr 0.4.6 → 0.4.8

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 (35) hide show
  1. package/README.md +8 -0
  2. package/package.json +1 -1
  3. package/src/channel.ts +41 -2
  4. package/src/core/targets.ts +106 -17
  5. package/src/messaging/inbound/commands.ts +421 -51
  6. package/src/messaging/inbound/context-facts.ts +126 -14
  7. package/src/messaging/inbound/contracts.ts +24 -0
  8. package/src/messaging/inbound/dispatch-prep.ts +214 -39
  9. package/src/messaging/inbound/dispatch.ts +71 -5
  10. package/src/messaging/inbound/gate.ts +56 -86
  11. package/src/messaging/inbound/group-history.ts +189 -0
  12. package/src/messaging/inbound/native-command-runtime.ts +77 -61
  13. package/src/messaging/inbound/native-command.ts +192 -8
  14. package/src/messaging/inbound/parse.ts +113 -8
  15. package/src/messaging/inbound/reply-dispatch-serial.ts +62 -0
  16. package/src/messaging/inbound/reply-dispatch.ts +252 -77
  17. package/src/messaging/inbound/scene-admin.ts +300 -0
  18. package/src/messaging/inbound/session-label.ts +122 -13
  19. package/src/messaging/inbound/session-meta-task.ts +17 -0
  20. package/src/messaging/inbound/turn-context.ts +184 -71
  21. package/src/openclaw/channel-runtime-contracts.ts +1 -0
  22. package/src/plugin/channel-components.ts +35 -1
  23. package/src/plugin/channel-inbound-helpers.ts +9 -2
  24. package/src/plugin/channel-runtime-builders-delivery.ts +24 -1
  25. package/src/plugin/channel-runtime-types.ts +42 -0
  26. package/src/plugin/file-inbound-init.ts +27 -12
  27. package/src/plugin/file-inbound-runtime.ts +2 -0
  28. package/src/plugin/inbound-acceptance.ts +82 -1
  29. package/src/plugin/inbound-handlers.ts +97 -2
  30. package/src/plugin/inbound-surface-handlers-group.ts +16 -0
  31. package/src/plugin/messaging.ts +22 -5
  32. package/src/plugin/scene-registry.ts +171 -0
  33. package/src/plugin/state-store.ts +133 -0
  34. package/src/plugin/state-transient-runtime-group.ts +5 -0
  35. package/src/plugin/target-runtime.ts +2 -2
@@ -20,7 +20,15 @@ import type {
20
20
  } from './contracts.ts';
21
21
  import type { ParsedInbound } from './dispatch-prep.ts';
22
22
  import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
23
- import { parseBncrNativeCommand, resolveBncrNativeVerboseCommand } from './native-command.ts';
23
+ import {
24
+ parseBncrNativeCommand,
25
+ parseBncrUnsupportedDirectCommand,
26
+ resolveBncrNativeHelpCommand,
27
+ resolveBncrNativeSessionResetCommand,
28
+ resolveBncrNativeStatusCommand,
29
+ resolveBncrNativeVerboseCommand,
30
+ resolveBncrNativeWhoamiCommand,
31
+ } from './native-command.ts';
24
32
  import {
25
33
  buildBncrNativeCommandSessionState,
26
34
  buildBncrNativeCommandSummary,
@@ -34,11 +42,13 @@ import {
34
42
  } from './native-command-runtime.ts';
35
43
  import { buildBncrReplyConfig } from './reply-config.ts';
36
44
  import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
45
+ import { executeSceneAdminCommand, parseSceneAdminCommand } from './scene-admin.ts';
37
46
  import {
38
47
  buildBncrInboundSessionIdentityPatch,
39
48
  recordAndPatchBncrInboundSessionEntry,
40
49
  wrapBncrInboundRecordSessionLabelCorrection,
41
50
  } from './session-label.ts';
51
+ import { createBncrSessionMetaTaskBarrier } from './session-meta-task.ts';
42
52
 
43
53
  function assertResolvedAgentRoute(resolvedRoute: OpenClawResolvedAgentRoute): {
44
54
  sessionKey: string;
@@ -65,8 +75,9 @@ function buildBncrNativeCommandResolvedRoute(args: {
65
75
  channelId: string;
66
76
  accountId: string;
67
77
  peer: ParsedInbound['peer'];
78
+ resolvedAgentId?: string;
68
79
  }) {
69
- return assertResolvedAgentRoute(
80
+ const resolvedRoute = assertResolvedAgentRoute(
70
81
  resolveOpenClawAgentRoute(args.api, {
71
82
  cfg: args.cfg,
72
83
  channel: args.channelId,
@@ -74,6 +85,14 @@ function buildBncrNativeCommandResolvedRoute(args: {
74
85
  peer: args.peer,
75
86
  }),
76
87
  );
88
+
89
+ const agentId = (args.resolvedAgentId || '').trim() || resolvedRoute.agentId;
90
+ if (!agentId || agentId === resolvedRoute.agentId) return resolvedRoute;
91
+
92
+ return {
93
+ ...resolvedRoute,
94
+ agentId,
95
+ };
77
96
  }
78
97
 
79
98
  export { parseBncrNativeCommand } from './native-command.ts';
@@ -84,6 +103,11 @@ export async function handleBncrNativeCommand(params: {
84
103
  cfg: BncrInboundConfig;
85
104
  parsed: ParsedInbound;
86
105
  canonicalAgentId: string;
106
+ resolvedAgentId?: string;
107
+ sceneRegistry: Map<string, import('../../plugin/channel-runtime-types.ts').BncrSceneRecord>;
108
+ defaultAdminAgentId: string;
109
+ defaultPublicAgentId: string;
110
+ now: () => number;
87
111
  rememberSessionRoute: BncrRememberSessionRoute;
88
112
  enqueueFromReply: BncrEnqueueFromReply;
89
113
  logger?: BncrInboundLogger;
@@ -91,10 +115,65 @@ export async function handleBncrNativeCommand(params: {
91
115
  | { handled: false }
92
116
  | { handled: true; command: string; sessionKey: string; fallbackToAgent?: boolean }
93
117
  > {
94
- const { api, channelId, cfg, parsed, canonicalAgentId, rememberSessionRoute, enqueueFromReply } =
95
- params;
118
+ const {
119
+ api,
120
+ channelId,
121
+ cfg,
122
+ parsed,
123
+ canonicalAgentId,
124
+ resolvedAgentId,
125
+ sceneRegistry,
126
+ defaultAdminAgentId,
127
+ defaultPublicAgentId,
128
+ now,
129
+ rememberSessionRoute,
130
+ enqueueFromReply,
131
+ } = params;
96
132
  const { accountId, route, peer, clientId, extracted, msgId } = parsed;
97
- const command = parseBncrNativeCommand(extracted.text);
133
+ const command = parseBncrNativeCommand(extracted.text, {
134
+ allowBareWhoami: parsed.isAdmin !== true,
135
+ allowBareStatus: parsed.isAdmin !== true && parsed.peer.kind === 'direct',
136
+ allowBareSessionReset: parsed.isAdmin !== true && parsed.peer.kind === 'direct',
137
+ });
138
+ if (!command && parsed.peer.kind === 'direct') {
139
+ const unsupportedDirectCommand = parseBncrUnsupportedDirectCommand(extracted.text);
140
+ if (unsupportedDirectCommand) {
141
+ const rejectedRoute = buildBncrNativeCommandResolvedRoute({
142
+ api,
143
+ cfg,
144
+ channelId,
145
+ accountId,
146
+ peer,
147
+ resolvedAgentId,
148
+ });
149
+ const { baseSessionKey, sessionKey } = buildBncrNativeCommandSessionState({
150
+ parsed,
151
+ sessionAgentId: rejectedRoute.agentId || canonicalAgentId,
152
+ resolvedRoute: rejectedRoute,
153
+ });
154
+ const displayTo = formatDisplayScope(route);
155
+ logBncrNativeCommandSummary(
156
+ buildBncrNativeCommandSummary({
157
+ kind: 'unsupported',
158
+ command: unsupportedDirectCommand.command,
159
+ accountId,
160
+ to: displayTo,
161
+ msgId: msgId || null,
162
+ result: 'rejected',
163
+ }),
164
+ );
165
+ await enqueueFromReply({
166
+ accountId,
167
+ sessionKey: baseSessionKey,
168
+ route,
169
+ payload: {
170
+ text: `Unsupported private-chat command: /${unsupportedDirectCommand.command}`,
171
+ replyToId: msgId || undefined,
172
+ },
173
+ });
174
+ return { handled: true, command: unsupportedDirectCommand.command, sessionKey };
175
+ }
176
+ }
98
177
  if (!command) return { handled: false };
99
178
  const nativeCommandDebugEnabled = resolveNativeCommandDebugEnabled({ cfg, channelId });
100
179
 
@@ -115,12 +194,13 @@ export async function handleBncrNativeCommand(params: {
115
194
  channelId,
116
195
  accountId,
117
196
  peer,
197
+ resolvedAgentId,
118
198
  });
119
199
 
120
200
  const { baseSessionKey, taskSessionKey, sessionKey, displayTo, originatingTo } =
121
201
  buildBncrNativeCommandSessionState({
122
202
  parsed,
123
- canonicalAgentId,
203
+ sessionAgentId: resolvedRoute.agentId || canonicalAgentId,
124
204
  resolvedRoute,
125
205
  });
126
206
  rememberSessionRoute(baseSessionKey, accountId, route);
@@ -133,8 +213,8 @@ export async function handleBncrNativeCommand(params: {
133
213
  '[bncr] inbound missing clientId for native command identity; using route identity fallback',
134
214
  );
135
215
  }
136
- const senderIdForContext = clientId || displayTo;
137
- const senderDisplayName = clientId ? 'bncr-client' : displayTo;
216
+ const senderIdForContext = parsed.userId || clientId || displayTo;
217
+ const senderDisplayName = parsed.userName || displayTo;
138
218
  const storePath = resolveBncrInboundSessionStorePath({
139
219
  storeConfig: cfg?.session?.store,
140
220
  agentId: resolvedRoute.agentId,
@@ -166,7 +246,217 @@ export async function handleBncrNativeCommand(params: {
166
246
  });
167
247
 
168
248
  const nativeVerbose = resolveBncrNativeVerboseCommand(command);
249
+ const nativeHelp = resolveBncrNativeHelpCommand(command);
250
+ const nativeWhoami = resolveBncrNativeWhoamiCommand({
251
+ command,
252
+ platform: parsed.platform,
253
+ groupId: parsed.groupId,
254
+ groupName: parsed.groupName,
255
+ userId: parsed.userId,
256
+ userName: parsed.userName,
257
+ isGroup: parsed.isGroup,
258
+ isAdmin: parsed.isAdmin,
259
+ });
260
+ const nativeStatus = resolveBncrNativeStatusCommand({
261
+ command,
262
+ accountId,
263
+ platform: parsed.platform,
264
+ userId: parsed.userId,
265
+ userName: parsed.userName,
266
+ resolvedAgentId: resolvedRoute.agentId || canonicalAgentId,
267
+ sessionKey,
268
+ });
269
+ const nativeSessionReset = resolveBncrNativeSessionResetCommand({ command });
270
+ if (nativeHelp) {
271
+ logBncrNativeCommandSummary(
272
+ buildBncrNativeCommandSummary({
273
+ kind: 'help',
274
+ command: command.command,
275
+ accountId,
276
+ to: displayTo,
277
+ msgId: msgId || null,
278
+ result: 'handled',
279
+ }),
280
+ );
281
+ await recordAndPatchBncrInboundSessionEntry({
282
+ storePath,
283
+ sessionKey,
284
+ ctx: ctxPayload,
285
+ patch: sessionIdentityPatch,
286
+ });
287
+ rememberSessionRoute(baseSessionKey, accountId, route);
288
+ await enqueueFromReply({
289
+ accountId,
290
+ sessionKey,
291
+ route,
292
+ payload: {
293
+ text: nativeHelp.text,
294
+ replyToId: msgId || undefined,
295
+ },
296
+ });
297
+ return { handled: true, command: command.command, sessionKey };
298
+ }
299
+
300
+ if (nativeWhoami) {
301
+ logBncrNativeCommandSummary(
302
+ buildBncrNativeCommandSummary({
303
+ kind: 'whoami',
304
+ command: command.command,
305
+ accountId,
306
+ to: displayTo,
307
+ msgId: msgId || null,
308
+ result: 'handled',
309
+ }),
310
+ );
311
+ await recordAndPatchBncrInboundSessionEntry({
312
+ storePath,
313
+ sessionKey,
314
+ ctx: ctxPayload,
315
+ patch: sessionIdentityPatch,
316
+ });
317
+ rememberSessionRoute(baseSessionKey, accountId, route);
318
+ await enqueueFromReply({
319
+ accountId,
320
+ sessionKey,
321
+ route,
322
+ payload: {
323
+ text: nativeWhoami.text,
324
+ replyToId: msgId || undefined,
325
+ },
326
+ });
327
+ return { handled: true, command: command.command, sessionKey };
328
+ }
329
+
330
+ if (nativeStatus) {
331
+ logBncrNativeCommandSummary(
332
+ buildBncrNativeCommandSummary({
333
+ kind: 'status',
334
+ command: command.command,
335
+ accountId,
336
+ to: displayTo,
337
+ msgId: msgId || null,
338
+ result: 'handled',
339
+ }),
340
+ );
341
+ await recordAndPatchBncrInboundSessionEntry({
342
+ storePath,
343
+ sessionKey,
344
+ ctx: ctxPayload,
345
+ patch: sessionIdentityPatch,
346
+ });
347
+ rememberSessionRoute(baseSessionKey, accountId, route);
348
+ await enqueueFromReply({
349
+ accountId,
350
+ sessionKey,
351
+ route,
352
+ payload: {
353
+ text: nativeStatus.text,
354
+ replyToId: msgId || undefined,
355
+ },
356
+ });
357
+ return { handled: true, command: command.command, sessionKey };
358
+ }
359
+
360
+ if (nativeSessionReset) {
361
+ const allowLocalSessionReset = parsed.peer.kind === 'direct';
362
+ if (!allowLocalSessionReset) {
363
+ logBncrNativeCommandSummary(
364
+ buildBncrNativeCommandSummary({
365
+ kind: nativeSessionReset.reason,
366
+ command: command.command,
367
+ accountId,
368
+ to: displayTo,
369
+ msgId: msgId || null,
370
+ result: 'rejected',
371
+ }),
372
+ );
373
+ await recordAndPatchBncrInboundSessionEntry({
374
+ storePath,
375
+ sessionKey,
376
+ ctx: ctxPayload,
377
+ patch: sessionIdentityPatch,
378
+ });
379
+ rememberSessionRoute(baseSessionKey, accountId, route);
380
+ return { handled: true, command: command.command, sessionKey };
381
+ }
382
+
383
+ const requestedAgentId = resolvedRoute.agentId || canonicalAgentId;
384
+ const requestApi = api as BncrInboundApi & {
385
+ request?: (method: string, params: Record<string, unknown>) => Promise<unknown>;
386
+ };
387
+ if (typeof requestApi.request !== 'function') {
388
+ throw new Error('OpenClaw plugin api.request is unavailable for sessions.reset');
389
+ }
390
+ await recordAndPatchBncrInboundSessionEntry({
391
+ storePath,
392
+ sessionKey,
393
+ ctx: ctxPayload,
394
+ patch: sessionIdentityPatch,
395
+ });
396
+ const resetResult = (await requestApi.request('sessions.reset', {
397
+ key: baseSessionKey,
398
+ reason: nativeSessionReset.reason,
399
+ ...(requestedAgentId ? { agentId: requestedAgentId } : {}),
400
+ })) as { ok?: boolean } | undefined;
401
+ if (resetResult?.ok === false) {
402
+ throw new Error(`sessions.reset failed for ${baseSessionKey}`);
403
+ }
404
+ logBncrNativeCommandSummary(
405
+ buildBncrNativeCommandSummary({
406
+ kind: nativeSessionReset.reason,
407
+ command: command.command,
408
+ accountId,
409
+ to: displayTo,
410
+ msgId: msgId || null,
411
+ result: 'handled',
412
+ }),
413
+ );
414
+ rememberSessionRoute(baseSessionKey, accountId, route);
415
+ await enqueueFromReply({
416
+ accountId,
417
+ sessionKey: baseSessionKey,
418
+ route,
419
+ payload: {
420
+ text: nativeSessionReset.text,
421
+ replyToId: msgId || undefined,
422
+ },
423
+ });
424
+ return { handled: true, command: command.command, sessionKey: baseSessionKey };
425
+ }
426
+
169
427
  if (nativeVerbose) {
428
+ if (!parsed.isAdmin) {
429
+ logBncrNativeCommandSummary(
430
+ buildBncrNativeCommandSummary({
431
+ kind: 'verbose',
432
+ command: command.command,
433
+ accountId,
434
+ to: displayTo,
435
+ msgId: msgId || null,
436
+ result: 'rejected',
437
+ }),
438
+ );
439
+ await recordAndPatchBncrInboundSessionEntry({
440
+ storePath,
441
+ sessionKey,
442
+ ctx: ctxPayload,
443
+ patch: sessionIdentityPatch,
444
+ });
445
+ rememberSessionRoute(baseSessionKey, accountId, route);
446
+ if (parsed.peer.kind !== 'group') {
447
+ await enqueueFromReply({
448
+ accountId,
449
+ sessionKey,
450
+ route,
451
+ payload: {
452
+ text: 'Admin permission required.',
453
+ replyToId: msgId || undefined,
454
+ },
455
+ });
456
+ }
457
+ return { handled: true, command: command.command, sessionKey };
458
+ }
459
+
170
460
  logBncrNativeCommandSummary(
171
461
  buildBncrNativeCommandSummary({
172
462
  kind: 'verbose',
@@ -211,6 +501,78 @@ export async function handleBncrNativeCommand(params: {
211
501
  return { handled: true, command: command.command, sessionKey };
212
502
  }
213
503
 
504
+ const sceneAdmin = parseSceneAdminCommand(command);
505
+ if (sceneAdmin.matched) {
506
+ const silentNonAdminGroupReject = !parsed.isAdmin && parsed.peer.kind === 'group';
507
+ if (!sceneAdmin.valid) {
508
+ logBncrNativeCommandSummary(
509
+ buildBncrNativeCommandSummary({
510
+ kind: 'scene-admin',
511
+ command: command.command,
512
+ accountId,
513
+ to: displayTo,
514
+ msgId: msgId ?? null,
515
+ result: 'rejected',
516
+ }),
517
+ );
518
+ await recordAndPatchBncrInboundSessionEntry({
519
+ storePath,
520
+ sessionKey,
521
+ ctx: ctxPayload,
522
+ patch: sessionIdentityPatch,
523
+ });
524
+ if (!silentNonAdminGroupReject) {
525
+ await enqueueFromReply({
526
+ accountId,
527
+ sessionKey,
528
+ route,
529
+ payload: {
530
+ text: sceneAdmin.text,
531
+ replyToId: msgId || undefined,
532
+ },
533
+ });
534
+ }
535
+ return { handled: true, command: command.command, sessionKey };
536
+ }
537
+
538
+ const outcome = executeSceneAdminCommand({
539
+ parsed,
540
+ command: sceneAdmin.command,
541
+ sceneRegistry,
542
+ defaultAdminAgentId,
543
+ defaultPublicAgentId,
544
+ now,
545
+ });
546
+ logBncrNativeCommandSummary(
547
+ buildBncrNativeCommandSummary({
548
+ kind: 'scene-admin',
549
+ command: command.command,
550
+ accountId,
551
+ to: displayTo,
552
+ msgId: msgId ?? null,
553
+ result: outcome.ok ? 'handled' : 'rejected',
554
+ }),
555
+ );
556
+ await recordAndPatchBncrInboundSessionEntry({
557
+ storePath,
558
+ sessionKey,
559
+ ctx: ctxPayload,
560
+ patch: sessionIdentityPatch,
561
+ });
562
+ if (!(silentNonAdminGroupReject && !outcome.ok)) {
563
+ await enqueueFromReply({
564
+ accountId,
565
+ sessionKey,
566
+ route,
567
+ payload: {
568
+ text: outcome.text,
569
+ replyToId: msgId || undefined,
570
+ },
571
+ });
572
+ }
573
+ return { handled: true, command: command.command, sessionKey };
574
+ }
575
+
214
576
  await recordAndPatchBncrInboundSessionEntry({
215
577
  storePath,
216
578
  sessionKey,
@@ -264,51 +626,59 @@ export async function handleBncrNativeCommand(params: {
264
626
  textForCommands: ctxPayload.CommandBody,
265
627
  raw: parsed,
266
628
  }),
267
- resolveTurn: () => ({
268
- channel: channelId,
269
- accountId,
270
- routeSessionKey: resolvedRoute.sessionKey,
271
- storePath,
272
- ctxPayload,
273
- recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
274
- recordInboundSession: recordBncrInboundSession as (
275
- ...args: unknown[]
276
- ) => Promise<unknown> | unknown,
277
- expectedLabel: displayTo,
278
- }),
279
- record: {
280
- updateLastRoute,
281
- onRecordError: (err: unknown) => {
282
- buildNativeCommandRecordErrorLogger(err);
283
- },
284
- },
285
- runDispatch: () =>
286
- dispatchOpenClawReplyWithBufferedBlockDispatcher(api, {
287
- ctx: ctxPayload,
288
- cfg: effectiveReply.replyCfg,
289
- dispatcherOptions: {
290
- deliver: createNativeCommandReplyDeliverer({
291
- command: command.command,
292
- accountId,
293
- sessionKey,
294
- to: displayTo,
295
- msgId: msgId || undefined,
296
- effectiveReply,
297
- route,
298
- enqueueFromReply,
299
- nativeCommandDebugEnabled,
300
- onResponded: () => responded,
301
- markResponded: () => {
302
- responded = true;
303
- },
304
- }),
629
+ resolveTurn: () => {
630
+ const sessionMetaBarrier = createBncrSessionMetaTaskBarrier();
631
+ return {
632
+ channel: channelId,
633
+ accountId,
634
+ routeSessionKey: resolvedRoute.sessionKey,
635
+ storePath,
636
+ ctxPayload,
637
+ recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
638
+ recordInboundSession: recordBncrInboundSession as (
639
+ ...args: unknown[]
640
+ ) => Promise<unknown> | unknown,
641
+ expectedPatch: sessionIdentityPatch,
642
+ }),
643
+ record: {
644
+ updateLastRoute,
645
+ onRecordError: (err: unknown) => {
646
+ buildNativeCommandRecordErrorLogger(err);
305
647
  },
306
- replyOptions: {
307
- disableBlockStreaming: !effectiveReply.blockStreaming,
308
- shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
648
+ trackSessionMetaTask: (task: Promise<unknown>) => {
649
+ sessionMetaBarrier.track(task);
309
650
  },
310
- }),
311
- }),
651
+ },
652
+ runDispatch: async () => {
653
+ await sessionMetaBarrier.wait();
654
+ return dispatchOpenClawReplyWithBufferedBlockDispatcher(api, {
655
+ ctx: ctxPayload,
656
+ cfg: effectiveReply.replyCfg,
657
+ dispatcherOptions: {
658
+ deliver: createNativeCommandReplyDeliverer({
659
+ command: command.command,
660
+ accountId,
661
+ sessionKey,
662
+ to: displayTo,
663
+ msgId: msgId || undefined,
664
+ effectiveReply,
665
+ route,
666
+ enqueueFromReply,
667
+ nativeCommandDebugEnabled,
668
+ onResponded: () => responded,
669
+ markResponded: () => {
670
+ responded = true;
671
+ },
672
+ }),
673
+ },
674
+ replyOptions: {
675
+ disableBlockStreaming: !effectiveReply.blockStreaming,
676
+ shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
677
+ },
678
+ });
679
+ },
680
+ };
681
+ },
312
682
  },
313
683
  });
314
684