@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
package/README.md CHANGED
@@ -93,9 +93,73 @@ openclaw devices approve --latest
93
93
  - 入站会话按正式 scene / route 语义接入 OpenClaw,区分私聊与群聊会话。
94
94
  - 非管理员私聊支持受控的本地命令能力:`/whoami`、`/status`、`/new`、`/reset`,以及对应的 `/bncr ...` 入口。
95
95
  - 非管理员私聊中,不在支持范围内的 slash 命令会由 bncr 直接拒绝,不再 fallback 到 agent。
96
- - 群聊里的管理型 / 控制型命令对非管理员按静默拒绝处理,不给用户可见回复。
96
+ - 群聊非管理员执行场景管理命令(`mode` / `allow` / `deny` / `bind` / `revoke` / `list` / `history-*` / `download-media`)时,统一返回 `Admin permission required.`,不再附带用法提示。
97
+ - 群聊非管理员执行 `/bncr new`、`/bncr reset`、`/bncr verbose` 时仍按静默拒绝处理。
97
98
  - `/bncr list pending` 与 `/bncr list scenes` 采用按私聊 / 群聊分组的摘要展示格式,便于现场排查 scene 状态。
98
99
 
100
+ ### `/bncr help` 权限过滤
101
+
102
+ `/bncr help` 现在按当前身份动态展示:
103
+
104
+ - 管理员:展示全部命令
105
+ - 私聊非管理员:展示当前可用命令,不展示管理类命令
106
+ - 群聊非管理员:仅展示基础命令,不展示 `mode`、`history-*`、`download-media` 等管理命令
107
+
108
+ 示例:
109
+
110
+ 管理员:
111
+
112
+ ```text
113
+ 🦞 Bncr command usage
114
+
115
+ 📌 Bncr builtins
116
+ • /bncr whoami
117
+ • /bncr status
118
+ • /bncr new
119
+ • /bncr reset
120
+ • /bncr verbose on|off|full
121
+
122
+ 🛡 Scene approval
123
+ • /bncr allow [<SceneId>]
124
+ • /bncr deny [<SceneId>]
125
+ • /bncr bind <agentId> [<SceneId>]
126
+ • /bncr mode help
127
+ • /bncr mode <admin|mention|hybrid|all|clear> [<SceneId>]
128
+ • /bncr revoke [<SceneId>]
129
+ • /bncr list pending [filters...]
130
+ • /bncr list scenes [filters...]
131
+
132
+ 📋 Conversation history
133
+ • /bncr history-help
134
+ • /bncr history-limit [<number>|clear] [<SceneId>]
135
+ • /bncr history-force on|off|clear [<SceneId>]
136
+
137
+ 🌐 Remote media
138
+ • /bncr download-media on|off|clear|default on|off [<SceneId>]
139
+ ```
140
+
141
+ 私聊非管理员:
142
+
143
+ ```text
144
+ 🦞 Bncr command usage
145
+
146
+ 📌 Bncr builtins
147
+ • /bncr whoami
148
+ • /bncr status
149
+ • /bncr new
150
+ • /bncr reset
151
+ ```
152
+
153
+ 群聊非管理员:
154
+
155
+ ```text
156
+ 🦞 Bncr command usage
157
+
158
+ 📌 Bncr builtins
159
+ • /bncr whoami
160
+ • /bncr status
161
+ ```
162
+
99
163
  ---
100
164
 
101
165
  ## 5. 架构定位
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -158,6 +158,7 @@ const requiredSourceFiles = [
158
158
  'src/plugin/setup.ts',
159
159
  'src/plugin/state-transient-runtime-group.ts',
160
160
  'src/plugin/state-store.ts',
161
+ 'src/plugin/sqlite-state.ts',
161
162
  'src/plugin/target-runtime.ts',
162
163
  'src/plugin/target-status-runtime-group.ts',
163
164
  'src/plugin/transient-state-runtime.ts',
@@ -174,7 +175,9 @@ const requiredSourceFiles = [
174
175
  'src/bootstrap/register-runtime-singleton.ts',
175
176
  'src/core/value-sanitize.ts',
176
177
  'src/messaging/inbound/contracts.ts',
177
- 'src/messaging/inbound/group-history.ts',
178
+ 'src/messaging/inbound/conversation-history.ts',
179
+ 'src/messaging/inbound/conversation-history-serial.ts',
180
+ 'src/messaging/inbound/history-shard-worker.ts',
178
181
  'src/messaging/inbound/native-command-runtime.ts',
179
182
  'src/messaging/inbound/reply-dispatch-serial.ts',
180
183
  'src/messaging/inbound/scene-admin.ts',
@@ -63,7 +63,9 @@ const requiredSourceFiles = [
63
63
  'src/messaging/inbound/dispatch-prep.ts',
64
64
  'src/messaging/inbound/dispatch.ts',
65
65
  'src/messaging/inbound/gate.ts',
66
- 'src/messaging/inbound/group-history.ts',
66
+ 'src/messaging/inbound/conversation-history.ts',
67
+ 'src/messaging/inbound/conversation-history-serial.ts',
68
+ 'src/messaging/inbound/history-shard-worker.ts',
67
69
  'src/messaging/inbound/last-route.ts',
68
70
  'src/messaging/inbound/media-url-download.ts',
69
71
  'src/messaging/inbound/native-command-runtime.ts',
@@ -191,6 +193,7 @@ const requiredSourceFiles = [
191
193
  'src/plugin/scene-registry.ts',
192
194
  'src/plugin/setup.ts',
193
195
  'src/plugin/state-store.ts',
196
+ 'src/plugin/sqlite-state.ts',
194
197
  'src/plugin/state-transient-runtime-group.ts',
195
198
  'src/plugin/status-runtime.ts',
196
199
  'src/plugin/status.ts',
package/src/channel.ts CHANGED
@@ -43,9 +43,22 @@ import type {
43
43
  FileSendTransferState,
44
44
  OutboxEntry,
45
45
  } from './core/types.ts';
46
- import type { BncrGroupHistoryMap } from './messaging/inbound/group-history.ts';
46
+ import {
47
+ type BncrConversationHistoryMap,
48
+ resolveBncrHistoryLimit,
49
+ } from './messaging/inbound/conversation-history.ts';
50
+ import {
51
+ clearConversationHistorySerialLocks,
52
+ readConversationHistorySerialHistoryKeys,
53
+ } from './messaging/inbound/conversation-history-serial.ts';
54
+ import { dispatchBncrOutboundHistoryFlush } from './messaging/inbound/dispatch.ts';
55
+ import {
56
+ type BncrHistoryShardDispatchPayload,
57
+ processBncrHistoryShardSlot,
58
+ } from './messaging/inbound/history-shard-worker.ts';
47
59
  import type { BncrOutboundReplayCache } from './messaging/inbound/outbound-replay-cache.ts';
48
60
  import type { parseBncrInboundParams } from './messaging/inbound/parse.ts';
61
+ import { runBncrInboundReplyDispatch } from './messaging/inbound/reply-dispatch.ts';
49
62
  import { buildEnqueueFromReplyDebugInfo } from './messaging/outbound/diagnostics.ts';
50
63
  import { buildBncrMediaOutboundFrame } from './messaging/outbound/media.ts';
51
64
  import {
@@ -126,6 +139,8 @@ import {
126
139
  FILE_TRANSFER_ACK_TTL_MS,
127
140
  FILE_TRANSFER_KEEP_MS,
128
141
  FILE_TRANSFER_TERMINAL_KEEP_MS,
142
+ HISTORY_SHARD_WORKER_BATCH_LIMIT,
143
+ HISTORY_SHARD_WORKER_INTERVAL_MS,
129
144
  INBOUND_FILE_TRANSFER_MAX_BYTES,
130
145
  INBOUND_FILE_TRANSFER_MAX_CHUNKS,
131
146
  MAX_ACCOUNT_ACTIVITY_ENTRIES,
@@ -187,6 +202,7 @@ import {
187
202
  createBncrRuntimeAckObservabilityBuilder,
188
203
  } from './plugin/runtime-diagnostics-snapshot.ts';
189
204
  import { BNCR_SETUP_SURFACE } from './plugin/setup.ts';
205
+ import type { BncrHistoryShardRow } from './plugin/sqlite-state.ts';
190
206
  import { createBncrStateTransientRuntimeGroup } from './plugin/state-transient-runtime-group.ts';
191
207
  import { createBncrTargetStatusRuntimeGroup } from './plugin/target-status-runtime-group.ts';
192
208
  import { shouldEmitDedupLog as shouldEmitDedupLogFromRuntime } from './runtime/log-dedupe.ts';
@@ -293,7 +309,7 @@ class BncrBridgeRuntime {
293
309
  getSceneRegistry(): Map<string, BncrSceneRecord> {
294
310
  return this.sceneRegistry;
295
311
  }
296
- private groupHistories: BncrGroupHistoryMap = new Map();
312
+ private conversationHistories: BncrConversationHistoryMap = new Map();
297
313
  private outboundReplayCache: BncrOutboundReplayCache = new Map();
298
314
  private routeAliases = new Map<
299
315
  string,
@@ -340,6 +356,8 @@ class BncrBridgeRuntime {
340
356
  // Timers / background workers ---------------------------------------------
341
357
  private saveTimer: NodeJS.Timeout | null = null;
342
358
  private pushTimer: NodeJS.Timeout | null = null;
359
+ private historyShardWorkerTimer: NodeJS.Timeout | null = null;
360
+ private historyShardWorkerRunning = false;
343
361
  private pushDrainRunningAccounts = new Set<string>();
344
362
  private pushDrainRunningSinceByAccount = new Map<string, number>();
345
363
  private pushDrainStuckWarnedAtByAccount = new Map<string, number>();
@@ -836,6 +854,7 @@ class BncrBridgeRuntime {
836
854
  initializeCanonicalAgentId: (cfg) => this.initializeCanonicalAgentId(cfg),
837
855
  logWarn: (scope, message, options) => this.logWarn(scope, message, options),
838
856
  loadState: () => this.loadState(),
857
+ cutoverToSqlite: () => this.stateStore.cutoverToSqlite(),
839
858
  setDebugFlag: (value) => {
840
859
  BNCR_DEBUG_VERBOSE = value;
841
860
  },
@@ -850,6 +869,7 @@ class BncrBridgeRuntime {
850
869
  ctx,
851
870
  debug,
852
871
  );
872
+ this.startHistoryShardWorker();
853
873
  };
854
874
 
855
875
  stopService = async () => {
@@ -867,6 +887,7 @@ class BncrBridgeRuntime {
867
887
  }
868
888
 
869
889
  private cleanupRuntimeWaitersAndTimers(reason: string) {
890
+ this.stopHistoryShardWorker(reason);
870
891
  this.clientRpcRuntime.shutdown();
871
892
  cleanupBncrBridgeRuntime(
872
893
  {
@@ -1077,13 +1098,85 @@ class BncrBridgeRuntime {
1077
1098
  }
1078
1099
 
1079
1100
  private async loadState() {
1080
- await this.stateStore.loadState();
1101
+ // A hot bridge rebuild must not recover shards whose old serial task is
1102
+ // still uploading in this process. Normal process restarts have no active
1103
+ // keys and therefore recover every in-flight shard.
1104
+ const activeHistoryKeys = readConversationHistorySerialHistoryKeys();
1105
+ clearConversationHistorySerialLocks(
1106
+ 'runtime-restart',
1107
+ `${this.bridgeId}:${Date.now()}:${Math.random().toString(16).slice(2, 8)}`,
1108
+ );
1109
+ await this.stateStore.recoverInFlightHistoryShards(activeHistoryKeys);
1110
+ await this.stateStore.loadState(activeHistoryKeys);
1081
1111
  }
1082
1112
 
1083
1113
  private async flushState() {
1084
1114
  await this.stateStore.flushState();
1085
1115
  }
1086
1116
 
1117
+ private startHistoryShardWorker() {
1118
+ if (this.historyShardWorkerTimer) {
1119
+ clearInterval(this.historyShardWorkerTimer);
1120
+ this.historyShardWorkerTimer = null;
1121
+ }
1122
+ if (this.stopped) return;
1123
+ this.historyShardWorkerTimer = setInterval(() => {
1124
+ void this.tickHistoryShardWorker();
1125
+ }, HISTORY_SHARD_WORKER_INTERVAL_MS);
1126
+ this.historyShardWorkerTimer.unref?.();
1127
+ }
1128
+
1129
+ private stopHistoryShardWorker(reason: string) {
1130
+ if (this.historyShardWorkerTimer) {
1131
+ clearInterval(this.historyShardWorkerTimer);
1132
+ this.historyShardWorkerTimer = null;
1133
+ }
1134
+ this.logInfo(
1135
+ 'history-shard',
1136
+ `history-shard worker stopped|bridge=${this.bridgeId}|reason=${reason}`,
1137
+ { debugOnly: true },
1138
+ );
1139
+ }
1140
+
1141
+ private async tickHistoryShardWorker() {
1142
+ if (this.historyShardWorkerRunning || this.stopped) return;
1143
+ this.historyShardWorkerRunning = true;
1144
+ try {
1145
+ for (let i = 0; i < HISTORY_SHARD_WORKER_BATCH_LIMIT; i += 1) {
1146
+ const shard = this.stateStore.historyShardQueue.claimNextHistoryShard();
1147
+ if (!shard) break;
1148
+ await this.processHistoryShard(shard);
1149
+ }
1150
+ } catch (error) {
1151
+ this.logWarn('inbound', `history shard worker tick failed: ${String(error)}`);
1152
+ } finally {
1153
+ this.historyShardWorkerRunning = false;
1154
+ }
1155
+ }
1156
+
1157
+ private async processHistoryShard(shard: BncrHistoryShardRow) {
1158
+ const cfg = getOpenClawRuntimeConfig(this.api);
1159
+ await processBncrHistoryShardSlot({
1160
+ shard,
1161
+ historyShardQueue: this.stateStore.historyShardQueue,
1162
+ conversationHistories: this.conversationHistories,
1163
+ outboundReplayCache: this.outboundReplayCache,
1164
+ runDispatch: (payload: BncrHistoryShardDispatchPayload) =>
1165
+ runBncrInboundReplyDispatch({
1166
+ api: this.api,
1167
+ channelId: CHANNEL_ID,
1168
+ cfg,
1169
+ ...payload,
1170
+ setInboundActivity: (accountId, at) => {
1171
+ this.lastInboundByAccount.set(accountId, at);
1172
+ this.markActivity(accountId, at);
1173
+ },
1174
+ scheduleSave: () => this.scheduleSave(),
1175
+ enqueueFromReply: (args) => this.enqueueFromReply(args),
1176
+ }),
1177
+ });
1178
+ }
1179
+
1087
1180
  private resolveMessageAck(messageId: string, result: 'acked' | 'timeout' = 'acked') {
1088
1181
  return this.messageAckRuntime.resolveMessageAck(messageId, result);
1089
1182
  }
@@ -1901,7 +1994,7 @@ class BncrBridgeRuntime {
1901
1994
  maxSessionRouteEntries: MAX_SESSION_ROUTE_ENTRIES,
1902
1995
  maxAccountActivityEntries: MAX_ACCOUNT_ACTIVITY_ENTRIES,
1903
1996
  sceneRegistry: this.sceneRegistry,
1904
- groupHistories: this.groupHistories,
1997
+ conversationHistories: this.conversationHistories,
1905
1998
  outboundReplayCache: this.outboundReplayCache,
1906
1999
  outbox: this.outbox,
1907
2000
  getDeadLetter: () => this.deadLetter,
@@ -2168,6 +2261,21 @@ class BncrBridgeRuntime {
2168
2261
  deadLetterSinceStartByAccount: this.deadLetterSinceStartByAccount,
2169
2262
  lastOutboundByAccount: this.lastOutboundByAccount,
2170
2263
  outboundReplayCache: this.outboundReplayCache,
2264
+ conversationHistories: this.conversationHistories,
2265
+ resolveOutboundHistoryLimit: (entry) => {
2266
+ const platform = entry.route?.platform || '';
2267
+ const groupId = entry.route?.groupId || '0';
2268
+ const userId = entry.route?.userId || '0';
2269
+ const sceneKey = groupId !== '0' ? `${platform}:${groupId}` : `${platform}:${userId}`;
2270
+ return resolveBncrHistoryLimit(this.sceneRegistry.get(sceneKey)?.historyLimit);
2271
+ },
2272
+ resolveOutboundHistoryForce: (entry) => {
2273
+ const platform = entry.route?.platform || '';
2274
+ const groupId = entry.route?.groupId || '0';
2275
+ const userId = entry.route?.userId || '0';
2276
+ const sceneKey = groupId !== '0' ? `${platform}:${groupId}` : `${platform}:${userId}`;
2277
+ return this.sceneRegistry.get(sceneKey)?.historyForce !== false;
2278
+ },
2171
2279
  resolveOutboundSender: (entry) => {
2172
2280
  try {
2173
2281
  const account = resolveAccount(getOpenClawRuntimeConfig(this.api), entry.accountId);
@@ -2176,6 +2284,8 @@ class BncrBridgeRuntime {
2176
2284
  return { sender: 'OpenClaw', senderId: entry.accountId };
2177
2285
  }
2178
2286
  },
2287
+ onConversationHistoryOverflow: (entry, historyVersion) =>
2288
+ this.flushConversationHistoryFromOutbound(entry, historyVersion),
2179
2289
  isOutboundAckRequired: (accountId) => this.isOutboundAckRequired(accountId),
2180
2290
  scheduleSave: () => this.scheduleSave(),
2181
2291
  flushPushQueueBestEffort: (args) => this.flushPushQueueBestEffort(args),
@@ -2763,6 +2873,35 @@ class BncrBridgeRuntime {
2763
2873
  this.bridgeOutboxFacade.markRecentOutboundAcked(entry);
2764
2874
  }
2765
2875
 
2876
+ private flushConversationHistoryFromOutbound(entry: OutboxEntry, historyVersion?: number) {
2877
+ const cfg = getOpenClawRuntimeConfig(this.api);
2878
+ const canonicalAgentId = this.canonicalAgentId || 'main';
2879
+ void dispatchBncrOutboundHistoryFlush({
2880
+ api: this.api,
2881
+ channelId: CHANNEL_ID,
2882
+ cfg,
2883
+ entry,
2884
+ ...(historyVersion !== undefined ? { historyVersion } : {}),
2885
+ canonicalAgentId,
2886
+ sceneRegistry: this.sceneRegistry,
2887
+ conversationHistories: this.conversationHistories,
2888
+ outboundReplayCache: this.outboundReplayCache,
2889
+ defaultAdminAgentId: canonicalAgentId,
2890
+ defaultPublicAgentId: this.defaultPublicAgentId(),
2891
+ now,
2892
+ rememberSessionRoute: (sessionKey, accountId, route) =>
2893
+ this.rememberSessionRoute(sessionKey, accountId, route),
2894
+ enqueueFromReply: (args) => this.enqueueFromReply(args),
2895
+ setInboundActivity: (accountId, at) => {
2896
+ this.lastInboundByAccount.set(accountId, at);
2897
+ this.markActivity(accountId, at);
2898
+ },
2899
+ scheduleSave: () => this.scheduleSave(),
2900
+ }).catch((err) => {
2901
+ this.logWarn('inbound', `outbound history flush failed: ${String(err)}`);
2902
+ });
2903
+ }
2904
+
2766
2905
  private logEnqueueFromReply(args: {
2767
2906
  accountId: string;
2768
2907
  sessionKey: string;
@@ -2893,7 +3032,7 @@ class BncrBridgeRuntime {
2893
3032
  defaultAdminAgentId: (args) => this.defaultAdminAgentId(args),
2894
3033
  defaultPublicAgentId: () => this.defaultPublicAgentId(),
2895
3034
  sceneRegistry: this.sceneRegistry,
2896
- groupHistories: this.groupHistories,
3035
+ conversationHistories: this.conversationHistories,
2897
3036
  outboundReplayCache: this.outboundReplayCache,
2898
3037
  prepareInboundAcceptance: (args) => this.prepareInboundAcceptance(args),
2899
3038
  logInboundSummary: (args) => this.logInboundSummary(args),
@@ -224,7 +224,10 @@ export async function handleBncrNativeCommand(params: {
224
224
  });
225
225
 
226
226
  const nativeVerbose = resolveBncrNativeVerboseCommand(command);
227
- const nativeHelp = resolveBncrNativeHelpCommand(command);
227
+ const nativeHelp = resolveBncrNativeHelpCommand(command, {
228
+ isAdmin: parsed.isAdmin,
229
+ peerKind: parsed.peer.kind as 'direct' | 'group',
230
+ });
228
231
  const nativeWhoami = resolveBncrNativeWhoamiCommand({
229
232
  command,
230
233
  platform: parsed.platform,
@@ -244,7 +247,10 @@ export async function handleBncrNativeCommand(params: {
244
247
  resolvedAgentId: resolvedRoute.agentId || canonicalAgentId,
245
248
  sessionKey,
246
249
  });
247
- const nativeSessionReset = resolveBncrNativeSessionResetCommand({ command });
250
+ const nativeSessionReset = resolveBncrNativeSessionResetCommand({
251
+ command,
252
+ peerKind: parsed.peer.kind as 'direct' | 'group',
253
+ });
248
254
  if (nativeHelp) {
249
255
  logBncrNativeCommandSummary(
250
256
  buildBncrNativeCommandSummary({
@@ -336,7 +342,7 @@ export async function handleBncrNativeCommand(params: {
336
342
  }
337
343
 
338
344
  if (nativeSessionReset) {
339
- const allowLocalSessionReset = parsed.peer.kind === 'direct';
345
+ const allowLocalSessionReset = parsed.peer.kind === 'direct' || parsed.isAdmin;
340
346
  if (!allowLocalSessionReset) {
341
347
  logBncrNativeCommandSummary(
342
348
  buildBncrNativeCommandSummary({
@@ -481,8 +487,7 @@ export async function handleBncrNativeCommand(params: {
481
487
 
482
488
  const sceneAdmin = parseSceneAdminCommand(command);
483
489
  if (sceneAdmin.matched) {
484
- const silentNonAdminGroupReject = !parsed.isAdmin && parsed.peer.kind === 'group';
485
- if (!sceneAdmin.valid) {
490
+ if (!parsed.isAdmin) {
486
491
  logBncrNativeCommandSummary(
487
492
  buildBncrNativeCommandSummary({
488
493
  kind: 'scene-admin',
@@ -493,17 +498,50 @@ export async function handleBncrNativeCommand(params: {
493
498
  result: 'rejected',
494
499
  }),
495
500
  );
496
- if (!silentNonAdminGroupReject) {
497
- await enqueueFromReply({
501
+ await recordAndPatchBncrInboundSessionEntry({
502
+ storePath,
503
+ sessionKey,
504
+ ctx: ctxPayload,
505
+ patch: sessionIdentityPatch,
506
+ });
507
+ await enqueueFromReply({
508
+ accountId,
509
+ sessionKey,
510
+ route,
511
+ payload: {
512
+ text: 'Admin permission required.',
513
+ replyToId: msgId || undefined,
514
+ },
515
+ });
516
+ return { handled: true, command: command.command, sessionKey };
517
+ }
518
+
519
+ if (!sceneAdmin.valid) {
520
+ logBncrNativeCommandSummary(
521
+ buildBncrNativeCommandSummary({
522
+ kind: 'scene-admin',
523
+ command: command.command,
498
524
  accountId,
499
- sessionKey,
500
- route,
501
- payload: {
502
- text: sceneAdmin.text,
503
- replyToId: msgId || undefined,
504
- },
505
- });
506
- }
525
+ to: displayTo,
526
+ msgId: msgId ?? null,
527
+ result: 'rejected',
528
+ }),
529
+ );
530
+ await recordAndPatchBncrInboundSessionEntry({
531
+ storePath,
532
+ sessionKey,
533
+ ctx: ctxPayload,
534
+ patch: sessionIdentityPatch,
535
+ });
536
+ await enqueueFromReply({
537
+ accountId,
538
+ sessionKey,
539
+ route,
540
+ payload: {
541
+ text: sceneAdmin.text,
542
+ replyToId: msgId || undefined,
543
+ },
544
+ });
507
545
  return { handled: true, command: command.command, sessionKey };
508
546
  }
509
547
 
@@ -531,17 +569,15 @@ export async function handleBncrNativeCommand(params: {
531
569
  ctx: ctxPayload,
532
570
  patch: sessionIdentityPatch,
533
571
  });
534
- if (!(silentNonAdminGroupReject && !outcome.ok)) {
535
- await enqueueFromReply({
536
- accountId,
537
- sessionKey,
538
- route,
539
- payload: {
540
- text: outcome.text,
541
- replyToId: msgId || undefined,
542
- },
543
- });
544
- }
572
+ await enqueueFromReply({
573
+ accountId,
574
+ sessionKey,
575
+ route,
576
+ payload: {
577
+ text: outcome.text,
578
+ replyToId: msgId || undefined,
579
+ },
580
+ });
545
581
  return { handled: true, command: command.command, sessionKey };
546
582
  }
547
583