@xmoxmo/bncr 0.3.5 → 0.3.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 (164) hide show
  1. package/README.md +5 -0
  2. package/dist/index.js +28 -5
  3. package/index.ts +55 -721
  4. package/package.json +8 -4
  5. package/scripts/check-pack.mjs +93 -18
  6. package/scripts/check-register-drift.mjs +35 -13
  7. package/scripts/selfcheck.mjs +80 -11
  8. package/src/bootstrap/channel-plugin-runtime.ts +81 -0
  9. package/src/bootstrap/cli.ts +97 -0
  10. package/src/bootstrap/register-runtime-gateway.ts +129 -0
  11. package/src/bootstrap/register-runtime-helpers.ts +140 -0
  12. package/src/bootstrap/register-runtime-singleton.ts +137 -0
  13. package/src/bootstrap/register-runtime.ts +201 -0
  14. package/src/bootstrap/runtime-discovery.ts +187 -0
  15. package/src/bootstrap/runtime-loader.ts +54 -0
  16. package/src/channel.ts +1590 -4967
  17. package/src/core/accounts.ts +23 -4
  18. package/src/core/dead-letter-diagnostics.ts +37 -5
  19. package/src/core/diagnostics.ts +31 -15
  20. package/src/core/downlink-health.ts +3 -11
  21. package/src/core/extended-diagnostics.ts +78 -36
  22. package/src/core/file-transfer-payloads.ts +1 -1
  23. package/src/core/logging.ts +1 -0
  24. package/src/core/outbox-enqueue.ts +13 -2
  25. package/src/core/outbox-entry-builders.ts +2 -0
  26. package/src/core/outbox-summary.ts +75 -3
  27. package/src/core/permissions.ts +15 -2
  28. package/src/core/persisted-outbox-entry.ts +21 -6
  29. package/src/core/policy.ts +45 -4
  30. package/src/core/probe.ts +3 -15
  31. package/src/core/register-trace.ts +3 -3
  32. package/src/core/status.ts +43 -4
  33. package/src/core/targets.ts +216 -205
  34. package/src/core/types.ts +221 -0
  35. package/src/core/value-sanitize.ts +29 -0
  36. package/src/messaging/inbound/commands.ts +147 -172
  37. package/src/messaging/inbound/context-facts.ts +4 -2
  38. package/src/messaging/inbound/contracts.ts +70 -0
  39. package/src/messaging/inbound/dispatch-prep.ts +303 -0
  40. package/src/messaging/inbound/dispatch.ts +49 -462
  41. package/src/messaging/inbound/gate.ts +18 -5
  42. package/src/messaging/inbound/last-route.ts +10 -4
  43. package/src/messaging/inbound/media-url-download.ts +109 -0
  44. package/src/messaging/inbound/native-command-runtime.ts +225 -0
  45. package/src/messaging/inbound/parse.ts +2 -1
  46. package/src/messaging/inbound/remote-media.ts +49 -0
  47. package/src/messaging/inbound/reply-config.ts +16 -4
  48. package/src/messaging/inbound/reply-dispatch.ts +162 -0
  49. package/src/messaging/inbound/runtime-compat.ts +31 -10
  50. package/src/messaging/inbound/session-label.ts +15 -7
  51. package/src/messaging/inbound/turn-context.ts +131 -0
  52. package/src/messaging/outbound/actions.ts +24 -10
  53. package/src/messaging/outbound/diagnostics-debug-builders.ts +365 -0
  54. package/src/messaging/outbound/diagnostics.ts +31 -355
  55. package/src/messaging/outbound/durable-message-adapter.ts +20 -16
  56. package/src/messaging/outbound/durable-queue-adapter.ts +20 -7
  57. package/src/messaging/outbound/media.ts +24 -13
  58. package/src/messaging/outbound/reply-enqueue-media.ts +181 -0
  59. package/src/messaging/outbound/reply-enqueue.ts +57 -134
  60. package/src/messaging/outbound/send-params.ts +3 -0
  61. package/src/messaging/outbound/send.ts +19 -10
  62. package/src/messaging/outbound/session-route.ts +18 -3
  63. package/src/openclaw/channel-runtime-contracts.ts +76 -0
  64. package/src/openclaw/config-runtime.ts +13 -7
  65. package/src/openclaw/inbound-session-runtime.ts +7 -3
  66. package/src/openclaw/ingress-runtime.ts +17 -27
  67. package/src/openclaw/reply-runtime.ts +54 -59
  68. package/src/openclaw/routing-runtime.ts +35 -18
  69. package/src/openclaw/runtime-surface.ts +156 -12
  70. package/src/openclaw/sdk-helpers.ts +8 -1
  71. package/src/openclaw/session-route-runtime.ts +12 -12
  72. package/src/plugin/ack-outbox-runtime-group.ts +264 -0
  73. package/src/plugin/bridge-ack-facade.ts +137 -0
  74. package/src/plugin/bridge-connection-facade.ts +111 -0
  75. package/src/plugin/bridge-diagnostics-facade.ts +23 -0
  76. package/src/plugin/bridge-drain-facade.ts +98 -0
  77. package/src/plugin/bridge-extended-diagnostics-facade.ts +149 -0
  78. package/src/plugin/bridge-file-transfer-push-facade.ts +140 -0
  79. package/src/plugin/bridge-lifecycle.ts +156 -0
  80. package/src/plugin/bridge-media-facade.ts +241 -0
  81. package/src/plugin/bridge-outbox-facade.ts +182 -0
  82. package/src/plugin/bridge-runtime-helpers.ts +266 -0
  83. package/src/plugin/bridge-runtime-snapshots.ts +104 -0
  84. package/src/plugin/bridge-runtime-surface-facade.ts +8 -0
  85. package/src/plugin/bridge-status-facade.ts +76 -0
  86. package/src/plugin/bridge-status-worker-facade.ts +72 -0
  87. package/src/plugin/bridge-support-runtime.ts +137 -0
  88. package/src/plugin/bridge-surface-handlers-group.ts +242 -0
  89. package/src/plugin/bridge-surface-helpers.ts +28 -0
  90. package/src/plugin/capabilities.ts +1 -3
  91. package/src/plugin/channel-components.ts +289 -0
  92. package/src/plugin/channel-inbound-helpers.ts +149 -0
  93. package/src/plugin/channel-plugin-bridge-group.ts +129 -0
  94. package/src/plugin/channel-plugin-surface-group.ts +202 -0
  95. package/src/plugin/channel-runtime-builders-delivery.ts +513 -0
  96. package/src/plugin/channel-runtime-builders-status.ts +331 -0
  97. package/src/plugin/channel-runtime-builders.ts +25 -0
  98. package/src/plugin/channel-runtime-constants.ts +40 -0
  99. package/src/plugin/channel-runtime-types.ts +146 -0
  100. package/src/plugin/channel-send-runtime-group.ts +37 -0
  101. package/src/plugin/channel-send.ts +226 -0
  102. package/src/plugin/channel-utils.ts +102 -0
  103. package/src/plugin/config.ts +24 -3
  104. package/src/plugin/connection-handlers-helpers.ts +254 -0
  105. package/src/plugin/connection-handlers.ts +440 -0
  106. package/src/plugin/connection-state-helpers.ts +159 -0
  107. package/src/plugin/connection-state-runtime-group.ts +51 -0
  108. package/src/plugin/connection-state.ts +527 -0
  109. package/src/plugin/diagnostics-handlers.ts +211 -0
  110. package/src/plugin/error-message.ts +15 -0
  111. package/src/plugin/file-ack-runtime.ts +284 -0
  112. package/src/plugin/file-inbound-abort.ts +112 -0
  113. package/src/plugin/file-inbound-chunk.ts +146 -0
  114. package/src/plugin/file-inbound-complete.ts +153 -0
  115. package/src/plugin/file-inbound-handlers.ts +19 -0
  116. package/src/plugin/file-inbound-init.ts +122 -0
  117. package/src/plugin/file-inbound-runtime.ts +51 -0
  118. package/src/plugin/file-inbound-state.ts +62 -0
  119. package/src/plugin/file-transfer-logs.ts +227 -0
  120. package/src/plugin/file-transfer-orchestrator-chunk.ts +135 -0
  121. package/src/plugin/file-transfer-orchestrator.ts +304 -0
  122. package/src/plugin/file-transfer-runtime-group.ts +102 -0
  123. package/src/plugin/file-transfer-send.ts +89 -0
  124. package/src/plugin/file-transfer-setup.ts +206 -0
  125. package/src/plugin/gateway-event-context.ts +41 -0
  126. package/src/plugin/gateway-runtime.ts +14 -4
  127. package/src/plugin/inbound-acceptance.ts +107 -0
  128. package/src/plugin/inbound-handlers.ts +248 -0
  129. package/src/plugin/inbound-surface-handlers-group.ts +152 -0
  130. package/src/plugin/media-dedupe-runtime.ts +90 -0
  131. package/src/plugin/media-orchestrators-runtime-group.ts +316 -0
  132. package/src/plugin/message-ack-runtime.ts +284 -0
  133. package/src/plugin/message-send.ts +16 -6
  134. package/src/plugin/messaging.ts +98 -36
  135. package/src/plugin/outbound.ts +50 -8
  136. package/src/plugin/outbox-ack-logs.ts +136 -0
  137. package/src/plugin/outbox-ack-outcome.ts +128 -0
  138. package/src/plugin/outbox-drain-ack.ts +145 -0
  139. package/src/plugin/outbox-drain-failure.ts +84 -0
  140. package/src/plugin/outbox-drain-loop.ts +554 -0
  141. package/src/plugin/outbox-drain-post-push.ts +159 -0
  142. package/src/plugin/outbox-drain-runtime.ts +141 -0
  143. package/src/plugin/outbox-drain-schedule.ts +116 -0
  144. package/src/plugin/outbox-file-push-flow.ts +69 -0
  145. package/src/plugin/outbox-push-route-runtime-group.ts +81 -0
  146. package/src/plugin/outbox-push.ts +267 -0
  147. package/src/plugin/outbox-route.ts +181 -0
  148. package/src/plugin/outbox-text-push-flow.ts +90 -0
  149. package/src/plugin/runtime-diagnostics-assembler.ts +183 -0
  150. package/src/plugin/runtime-diagnostics-helpers.ts +302 -0
  151. package/src/plugin/runtime-diagnostics-payload-builders.ts +171 -0
  152. package/src/plugin/runtime-diagnostics-snapshot.ts +31 -0
  153. package/src/plugin/setup.ts +33 -6
  154. package/src/plugin/state-store.ts +249 -0
  155. package/src/plugin/state-transient-runtime-group.ts +105 -0
  156. package/src/plugin/status-runtime.ts +251 -0
  157. package/src/plugin/status.ts +33 -7
  158. package/src/plugin/target-runtime.ts +141 -0
  159. package/src/plugin/target-status-runtime-group.ts +130 -0
  160. package/src/plugin/transient-state-runtime.ts +82 -0
  161. package/src/runtime/outbound-ack-timeout.ts +5 -3
  162. package/src/runtime/outbound-flags.ts +24 -8
  163. package/src/runtime/status-snapshots.ts +36 -7
  164. package/src/runtime/status-worker.ts +34 -4
package/src/core/probe.ts CHANGED
@@ -1,18 +1,4 @@
1
- function finiteNumberOr(value: unknown, fallback: number): number {
2
- if (value == null) return fallback;
3
- const n = Number(value);
4
- return Number.isFinite(n) ? n : fallback;
5
- }
6
-
7
- function finiteNumberOrNull(value: unknown): number | null {
8
- if (value == null) return null;
9
- const n = Number(value);
10
- return Number.isFinite(n) ? n : null;
11
- }
12
-
13
- function nonNegativeFiniteNumberOr(value: unknown, fallback: number): number {
14
- return Math.max(0, finiteNumberOr(value, fallback));
15
- }
1
+ import { finiteNumberOrNull, nonNegativeFiniteNumberOr } from './value-sanitize.ts';
16
2
 
17
3
  export function probeBncrAccount(params: {
18
4
  accountId: string;
@@ -64,3 +50,5 @@ export function probeBncrAccount(params: {
64
50
  },
65
51
  };
66
52
  }
53
+
54
+ export type BncrAccountProbe = ReturnType<typeof probeBncrAccount>;
@@ -35,9 +35,9 @@ export type RegisterTraceSummary = {
35
35
 
36
36
  export type RegisterDriftSnapshot = {
37
37
  capturedAt: number;
38
- registerCount: number;
39
- apiGeneration: number;
40
- postWarmupRegisterCount: number;
38
+ registerCount: number | null;
39
+ apiGeneration: number | null;
40
+ postWarmupRegisterCount: number | null;
41
41
  apiInstanceId: string | null;
42
42
  registryFingerprint: string | null;
43
43
  dominantBucket: string | null;
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { buildRuntimeStatusMetaDisplay } from './status-meta.ts';
4
- import type { BncrDiagnosticsSummary, PendingAdmission } from './types.ts';
4
+ import type { BncrDiagnosticsSummary, BncrRuntimeLastSession, PendingAdmission } from './types.ts';
5
5
 
6
6
  type RuntimeStatusInput = {
7
7
  accountId: string;
@@ -15,7 +15,7 @@ type RuntimeStatusInput = {
15
15
  ackEvents: number;
16
16
  startedAt: number;
17
17
  pendingAdmissions?: PendingAdmission[];
18
- lastSession?: { sessionKey: string; scope: string; updatedAt: number } | null;
18
+ lastSession?: BncrRuntimeLastSession | null;
19
19
  lastActivityAt?: number | null;
20
20
  lastInboundAt?: number | null;
21
21
  lastOutboundAt?: number | null;
@@ -140,9 +140,48 @@ export function buildAccountRuntimeSnapshot(input: RuntimeStatusInput) {
140
140
  };
141
141
  }
142
142
 
143
+ export type BncrAccountRuntimeSnapshot = ReturnType<typeof buildAccountRuntimeSnapshot>;
144
+
143
145
  export function buildAccountStatusSnapshot(input: {
144
146
  account: { accountId: string; name?: string; enabled?: boolean };
145
- runtime: any;
147
+ runtime:
148
+ | {
149
+ connected?: boolean;
150
+ running?: boolean;
151
+ mode?: string;
152
+ pending?: number | null;
153
+ deadLetter?: number | null;
154
+ lastEventAt?: number | null;
155
+ lastError?: string | null;
156
+ lastSessionKey?: string | null;
157
+ lastSessionScope?: string | null;
158
+ lastSessionAt?: number | null;
159
+ lastActivityAt?: number | null;
160
+ lastInboundAt?: number | null;
161
+ lastOutboundAt?: number | null;
162
+ lastSessionAgo?: string | null;
163
+ lastActivityAgo?: string | null;
164
+ lastInboundAgo?: string | null;
165
+ lastOutboundAgo?: string | null;
166
+ diagnostics?: BncrDiagnosticsSummary | Record<string, unknown> | null;
167
+ meta?: {
168
+ pending?: number | null;
169
+ deadLetter?: number | null;
170
+ lastSessionKey?: string | null;
171
+ lastSessionScope?: string | null;
172
+ lastSessionAt?: number | null;
173
+ lastActivityAt?: number | null;
174
+ lastInboundAt?: number | null;
175
+ lastOutboundAt?: number | null;
176
+ lastSessionAgo?: string | null;
177
+ lastActivityAgo?: string | null;
178
+ lastInboundAgo?: string | null;
179
+ lastOutboundAgo?: string | null;
180
+ diagnostics?: BncrDiagnosticsSummary | Record<string, unknown> | null;
181
+ } | null;
182
+ }
183
+ | null
184
+ | undefined;
146
185
  healthSummary: string;
147
186
  displayName: string;
148
187
  }) {
@@ -162,7 +201,7 @@ export function buildAccountStatusSnapshot(input: {
162
201
  const lastOutboundAt = rt?.lastOutboundAt ?? meta.lastOutboundAt ?? null;
163
202
  const lastOutboundAgo = rt?.lastOutboundAgo ?? meta.lastOutboundAgo ?? '-';
164
203
  const diagnostics = rt?.diagnostics ?? meta.diagnostics ?? null;
165
- const normalizedMode = rt?.mode === 'linked' ? 'linked' : 'Status';
204
+ const normalizedMode = rt?.mode === 'linked' ? 'linked' : 'configured';
166
205
 
167
206
  return {
168
207
  accountId: input.account.accountId,
@@ -1,6 +1,14 @@
1
1
  import type { BncrRoute } from './types.ts';
2
2
 
3
+ type RouteInputLike = {
4
+ platform?: unknown;
5
+ groupId?: unknown;
6
+ userId?: unknown;
7
+ route?: unknown;
8
+ };
9
+
3
10
  export type BncrSessionKind = 'direct' | 'group';
11
+
4
12
  export type BncrExplicitTarget = {
5
13
  raw: string;
6
14
  normalized: string;
@@ -20,22 +28,14 @@ export type BncrExplicitTarget = {
20
28
  groupId?: string;
21
29
  };
22
30
 
23
- function asString(v: unknown, fallback = ''): string {
24
- if (typeof v === 'string') return v;
25
- if (v == null) return fallback;
26
- return String(v);
27
- }
28
-
29
- export function parseRouteFromScope(scope: string): BncrRoute | null {
30
- const parts = asString(scope).trim().split(':');
31
- if (parts.length < 3) return null;
32
- const [platform, groupId, userId] = parts;
33
- if (!platform || !groupId || !userId) return null;
34
- return { platform, groupId, userId };
31
+ export function asTargetString(value: unknown, fallback = ''): string {
32
+ if (typeof value === 'string') return value;
33
+ if (value == null) return fallback;
34
+ return String(value);
35
35
  }
36
36
 
37
- function parseRouteFromStandardDisplayScope(scope: string): BncrRoute | null {
38
- const parts = asString(scope).trim().split(':');
37
+ export function parseRouteFromStandardDisplayScope(scope: string): BncrRoute | null {
38
+ const parts = asTargetString(scope).trim().split(':');
39
39
  if (parts.length === 2) {
40
40
  const [platform, userId] = parts;
41
41
  if (!platform || !userId) return null;
@@ -51,8 +51,8 @@ function parseRouteFromStandardDisplayScope(scope: string): BncrRoute | null {
51
51
  return null;
52
52
  }
53
53
 
54
- function normalizeDisplayScopePrefix(scope: string): string {
55
- const raw = asString(scope).trim();
54
+ export function normalizeDisplayScopePrefix(scope: string): string {
55
+ const raw = asTargetString(scope).trim();
56
56
  if (!raw) return '';
57
57
  if (raw.startsWith('Bncr:')) return raw;
58
58
  if (/^bncr[:-]/i.test(raw)) return raw;
@@ -60,108 +60,40 @@ function normalizeDisplayScopePrefix(scope: string): string {
60
60
  return `Bncr:${raw}`;
61
61
  }
62
62
 
63
- export function parseRouteFromDisplayScope(scope: string): BncrRoute | null {
64
- const raw = normalizeDisplayScopePrefix(scope);
65
- if (!raw) return null;
66
-
67
- const payload = raw.match(/^Bncr:(.+)$/)?.[1];
68
- if (!payload) return null;
69
- return parseRouteFromStandardDisplayScope(payload);
70
- }
71
-
72
- export function formatDisplayScope(route: BncrRoute): string {
73
- if (route.groupId === '0' && route.userId !== '0') {
74
- return `Bncr:${route.platform}:${route.userId}`;
75
- }
76
- return `Bncr:${route.platform}:${route.groupId}:${route.userId}`;
77
- }
78
-
79
- export function buildDisplayScopeCandidates(route: BncrRoute): string[] {
80
- const candidates = [formatDisplayScope(route)].filter(Boolean);
81
- return Array.from(new Set(candidates.map((x) => asString(x).trim()).filter(Boolean)));
82
- }
83
-
84
- export function formatTargetDisplay(
85
- input: BncrRoute | BncrExplicitTarget | null | undefined,
86
- ): string {
87
- if (!input) return '';
88
- const route = parseRouteLike((input as any)?.route) || parseRouteLike(input);
89
- if (!route) return '';
90
- return formatDisplayScope(route);
63
+ export function resolveCanonicalSessionKind(): BncrSessionKind {
64
+ return 'direct';
91
65
  }
92
66
 
93
- export function parseExplicitTarget(
94
- input: string,
95
- options?: { canonicalAgentId?: string | null },
96
- ): BncrExplicitTarget | null {
97
- const raw = asString(input).trim();
98
- if (!raw) return null;
99
-
100
- const canonicalAgentId = asString(options?.canonicalAgentId).trim() || undefined;
101
- let route: BncrRoute | null = null;
102
- let source: BncrExplicitTarget['source'] | null = null;
103
-
104
- const strict = parseStrictBncrSessionKey(raw);
105
- if (strict?.route) {
106
- route = strict.route;
107
- source = 'strict-session-key';
108
- }
109
-
110
- if (!route) {
111
- const displayRoute = parseRouteFromDisplayScope(raw);
112
- if (displayRoute) {
113
- route = displayRoute;
114
- source = 'display-scope';
115
- }
116
- }
117
-
118
- if (!route) {
119
- const legacy = parseLegacySessionKey(raw);
120
- if (legacy?.route) {
121
- route = legacy.route;
122
- source = legacy.source === 'hex' ? 'hex-scope' : 'legacy-session-key';
123
- }
124
- }
125
-
126
- if (!route) {
127
- const hexRoute = parseRouteFromHexScope(raw);
128
- if (hexRoute) {
129
- route = hexRoute;
130
- source = 'hex-scope';
131
- }
132
- }
133
-
134
- if (!route) {
135
- const scopedRoute = parseRouteFromScope(raw);
136
- if (scopedRoute) {
137
- route = scopedRoute;
138
- source = 'route-scope';
139
- }
140
- }
141
-
142
- if (!route || !source) return null;
143
-
144
- const kind: BncrSessionKind = route.groupId === '0' ? 'direct' : 'group';
145
- const displayScope = formatDisplayScope(route);
67
+ export function buildExplicitTargetResult(args: {
68
+ raw: string;
69
+ source:
70
+ | 'display-scope'
71
+ | 'strict-session-key'
72
+ | 'legacy-session-key'
73
+ | 'hex-scope'
74
+ | 'route-scope';
75
+ route: BncrRoute;
76
+ displayScope: string;
77
+ canonicalSessionKey?: string;
78
+ }) {
79
+ const kind: BncrSessionKind = args.route.groupId === '0' ? 'direct' : 'group';
146
80
  return {
147
- raw,
148
- normalized: displayScope,
149
- source,
81
+ raw: args.raw,
82
+ normalized: args.displayScope,
83
+ source: args.source,
150
84
  kind,
151
- chatType: 'direct',
152
- displayScope,
153
- route,
154
- ...(canonicalAgentId
155
- ? { canonicalSessionKey: buildCanonicalBncrSessionKey(route, canonicalAgentId) }
156
- : {}),
157
- platform: route.platform,
158
- userId: route.userId,
159
- ...(route.groupId === '0' ? {} : { groupId: route.groupId }),
85
+ chatType: 'direct' as const,
86
+ displayScope: args.displayScope,
87
+ route: args.route,
88
+ ...(args.canonicalSessionKey ? { canonicalSessionKey: args.canonicalSessionKey } : {}),
89
+ platform: args.route.platform,
90
+ userId: args.route.userId,
91
+ ...(args.route.groupId === '0' ? {} : { groupId: args.route.groupId }),
160
92
  };
161
93
  }
162
94
 
163
95
  export function isLowerHex(input: string): boolean {
164
- const raw = asString(input).trim();
96
+ const raw = asTargetString(input).trim();
165
97
  return !!raw && /^[0-9a-fA-F]+$/.test(raw) && raw.length % 2 === 0;
166
98
  }
167
99
 
@@ -170,8 +102,16 @@ export function routeScopeToHex(route: BncrRoute): string {
170
102
  return Buffer.from(raw, 'utf8').toString('hex').toLowerCase();
171
103
  }
172
104
 
105
+ export function parseRouteFromScope(scope: string): BncrRoute | null {
106
+ const parts = asTargetString(scope).trim().split(':');
107
+ if (parts.length < 3) return null;
108
+ const [platform, groupId, userId] = parts;
109
+ if (!platform || !groupId || !userId) return null;
110
+ return { platform, groupId, userId };
111
+ }
112
+
173
113
  export function parseRouteFromHexScope(scopeHex: string): BncrRoute | null {
174
- const rawHex = asString(scopeHex).trim();
114
+ const rawHex = asTargetString(scopeHex).trim();
175
115
  if (!isLowerHex(rawHex)) return null;
176
116
 
177
117
  try {
@@ -182,26 +122,42 @@ export function parseRouteFromHexScope(scopeHex: string): BncrRoute | null {
182
122
  }
183
123
  }
184
124
 
185
- export function parseRouteLike(input: unknown): BncrRoute | null {
186
- const platform = asString((input as any)?.platform || '').trim();
187
- const groupId = asString((input as any)?.groupId || '').trim();
188
- const userId = asString((input as any)?.userId || '').trim();
189
- if (!platform || !groupId || !userId) return null;
190
- return { platform, groupId, userId };
191
- }
125
+ export function parseStrictBncrSessionKey(input: string): {
126
+ inputSessionKey: string;
127
+ inputAgentId: string;
128
+ inputKind: BncrSessionKind;
129
+ scopeHex: string;
130
+ route: BncrRoute;
131
+ } | null {
132
+ const raw = asTargetString(input).trim();
133
+ if (!raw) return null;
192
134
 
193
- export function resolveCanonicalSessionKind(_input?: {
194
- route?: BncrRoute | null;
195
- scope?: string | null;
196
- sessionKey?: string | null;
197
- }): BncrSessionKind {
198
- return 'direct';
199
- }
135
+ const m = raw.match(/^agent:([^:]+):bncr:(direct|group):(.+)$/);
136
+ if (!m?.[1] || !m?.[2] || !m?.[3]) return null;
200
137
 
201
- export function buildCanonicalBncrSessionKey(route: BncrRoute, canonicalAgentId: string): string {
202
- const agentId = asString(canonicalAgentId).trim() || 'main';
203
- const kind = resolveCanonicalSessionKind({ route });
204
- return `agent:${agentId}:bncr:${kind}:${routeScopeToHex(route)}`;
138
+ const inputAgentId = asTargetString(m[1]).trim();
139
+ const inputKind = m[2] as BncrSessionKind;
140
+ const payload = asTargetString(m[3]).trim();
141
+ let route: BncrRoute | null = null;
142
+ let scopeHex = '';
143
+
144
+ if (isLowerHex(payload)) {
145
+ scopeHex = payload.toLowerCase();
146
+ route = parseRouteFromHexScope(scopeHex);
147
+ } else {
148
+ route = parseRouteFromScope(payload);
149
+ if (route) scopeHex = routeScopeToHex(route);
150
+ }
151
+
152
+ if (!route || !scopeHex) return null;
153
+
154
+ return {
155
+ inputSessionKey: raw,
156
+ inputAgentId,
157
+ inputKind,
158
+ scopeHex,
159
+ route,
160
+ };
205
161
  }
206
162
 
207
163
  export function parseLegacySessionKey(input: string): {
@@ -210,7 +166,7 @@ export function parseLegacySessionKey(input: string): {
210
166
  inputAgentId?: string;
211
167
  source: 'legacy-direct' | 'legacy-bncr' | 'legacy-agent' | 'hex';
212
168
  } | null {
213
- const raw = asString(input).trim();
169
+ const raw = asTargetString(input).trim();
214
170
  if (!raw) return null;
215
171
 
216
172
  const directLegacy = raw.match(/^agent:([^:]+):bncr:direct:([0-9a-fA-F]+):0$/);
@@ -266,68 +222,36 @@ export function parseLegacySessionKey(input: string): {
266
222
  }
267
223
 
268
224
  export function isLegacyNoiseRoute(route: BncrRoute): boolean {
269
- const platform = asString(route.platform).trim().toLowerCase();
270
- const groupId = asString(route.groupId).trim().toLowerCase();
271
- const userId = asString(route.userId).trim().toLowerCase();
225
+ const platform = asTargetString(route.platform).trim().toLowerCase();
226
+ const groupId = asTargetString(route.groupId).trim().toLowerCase();
227
+ const userId = asTargetString(route.userId).trim().toLowerCase();
272
228
 
273
229
  if (platform === 'agent' && groupId === 'main' && userId === 'bncr') return true;
274
230
  if (platform === 'bncr' && userId === '0' && isLowerHex(groupId)) return true;
275
231
  return false;
276
232
  }
277
233
 
278
- export function parseStrictBncrSessionKey(input: string): {
279
- inputSessionKey: string;
280
- inputAgentId: string;
281
- inputKind: BncrSessionKind;
282
- scopeHex: string;
283
- route: BncrRoute;
284
- } | null {
285
- const raw = asString(input).trim();
234
+ export function parseRouteFromDisplayScope(scope: string): BncrRoute | null {
235
+ const raw = normalizeDisplayScopePrefix(scope);
286
236
  if (!raw) return null;
287
237
 
288
- const m = raw.match(/^agent:([^:]+):bncr:(direct|group):(.+)$/);
289
- if (!m?.[1] || !m?.[2] || !m?.[3]) return null;
290
-
291
- const inputAgentId = asString(m[1]).trim();
292
- const inputKind = m[2] as BncrSessionKind;
293
- const payload = asString(m[3]).trim();
294
- let route: BncrRoute | null = null;
295
- let scopeHex = '';
296
-
297
- if (isLowerHex(payload)) {
298
- scopeHex = payload.toLowerCase();
299
- route = parseRouteFromHexScope(scopeHex);
300
- } else {
301
- route = parseRouteFromScope(payload);
302
- if (route) scopeHex = routeScopeToHex(route);
303
- }
304
-
305
- if (!route || !scopeHex) return null;
306
-
307
- return {
308
- inputSessionKey: raw,
309
- inputAgentId,
310
- inputKind,
311
- scopeHex,
312
- route,
313
- };
238
+ const payload = raw.match(/^Bncr:(.+)$/)?.[1];
239
+ if (!payload) return null;
240
+ return parseRouteFromStandardDisplayScope(payload);
314
241
  }
315
242
 
316
- export function normalizeTaskKey(input: unknown): string | null {
317
- const raw = asString(input).trim().toLowerCase();
318
- if (!raw) return null;
319
- const normalized = raw
320
- .replace(/[^a-z0-9_-]+/g, '-')
321
- .replace(/^-+|-+$/g, '')
322
- .slice(0, 32);
323
- return normalized || null;
243
+ export function buildCanonicalBncrSessionKey(route: BncrRoute, canonicalAgentId: string): string {
244
+ const agentId = asTargetString(canonicalAgentId).trim() || 'main';
245
+ const kind = resolveCanonicalSessionKind();
246
+ return `agent:${agentId}:bncr:${kind}:${routeScopeToHex(route)}`;
324
247
  }
325
248
 
326
249
  export function normalizeStoredSessionKey(
327
250
  input: string,
328
251
  canonicalAgentId?: string | null,
252
+ helpers?: { normalizeTaskKey?: (input: unknown) => string | null },
329
253
  ): { sessionKey: string; route: BncrRoute } | null {
330
- const raw = asString(input).trim();
254
+ const raw = asTargetString(input).trim();
331
255
  if (!raw) return null;
332
256
 
333
257
  let taskKey: string | null = null;
@@ -335,8 +259,8 @@ export function normalizeStoredSessionKey(
335
259
 
336
260
  const taskTagged = raw.match(/^(.*):task:([a-z0-9_-]{1,32})$/i);
337
261
  if (taskTagged) {
338
- base = asString(taskTagged[1]).trim();
339
- taskKey = normalizeTaskKey(taskTagged[2]);
262
+ base = asTargetString(taskTagged[1]).trim();
263
+ taskKey = helpers?.normalizeTaskKey?.(taskTagged[2]) || null;
340
264
  }
341
265
 
342
266
  let route: BncrRoute | null = null;
@@ -359,7 +283,7 @@ export function normalizeStoredSessionKey(
359
283
  if (!route) return null;
360
284
  if (isLegacyNoiseRoute(route)) return null;
361
285
 
362
- const finalAgentId = asString(canonicalAgentId).trim() || passthroughAgentId;
286
+ const finalAgentId = asTargetString(canonicalAgentId).trim() || passthroughAgentId;
363
287
  if (!finalAgentId) return null;
364
288
 
365
289
  const finalSessionKey = buildCanonicalBncrSessionKey(route, finalAgentId);
@@ -374,51 +298,132 @@ export function normalizeInboundSessionKey(
374
298
  route: BncrRoute,
375
299
  canonicalAgentId: string,
376
300
  ): string | null {
377
- const raw = asString(scope).trim();
301
+ const raw = asTargetString(scope).trim();
378
302
  let finalRoute: BncrRoute | null = null;
379
303
 
380
- if (!raw) {
381
- finalRoute = route;
304
+ if (!raw) finalRoute = route;
305
+ if (!finalRoute) finalRoute = parseStrictBncrSessionKey(raw)?.route || null;
306
+ if (!finalRoute) finalRoute = parseLegacySessionKey(raw)?.route || null;
307
+ if (!finalRoute) finalRoute = parseRouteFromDisplayScope(raw);
308
+ if (!finalRoute) finalRoute = parseRouteFromScope(raw);
309
+ if (!finalRoute && route) finalRoute = route;
310
+ if (!finalRoute) return null;
311
+
312
+ return buildCanonicalBncrSessionKey(finalRoute, canonicalAgentId);
313
+ }
314
+
315
+ export function buildFallbackSessionKey(route: BncrRoute, canonicalAgentId: string): string {
316
+ return buildCanonicalBncrSessionKey(route, canonicalAgentId);
317
+ }
318
+
319
+ export function parseRouteLike(input: unknown): BncrRoute | null {
320
+ const routeInput = input && typeof input === 'object' ? (input as RouteInputLike) : null;
321
+ const platform = asTargetString(routeInput?.platform || '').trim();
322
+ const groupId = asTargetString(routeInput?.groupId || '').trim();
323
+ const userId = asTargetString(routeInput?.userId || '').trim();
324
+ if (!platform || !groupId || !userId) return null;
325
+ return { platform, groupId, userId };
326
+ }
327
+
328
+ export type BncrExplicitTargetSource =
329
+ | 'display-scope'
330
+ | 'strict-session-key'
331
+ | 'legacy-session-key'
332
+ | 'hex-scope'
333
+ | 'route-scope';
334
+
335
+ export function formatDisplayScope(route: BncrRoute): string {
336
+ if (route.groupId === '0' && route.userId !== '0') {
337
+ return `Bncr:${route.platform}:${route.userId}`;
338
+ }
339
+ return `Bncr:${route.platform}:${route.groupId}:${route.userId}`;
340
+ }
341
+
342
+ export function buildDisplayScopeCandidates(route: BncrRoute): string[] {
343
+ const candidates = [formatDisplayScope(route)].filter(Boolean);
344
+ return Array.from(new Set(candidates.map((x) => asTargetString(x).trim()).filter(Boolean)));
345
+ }
346
+
347
+ export function resolveExplicitTargetRoute(raw: string): {
348
+ route: BncrRoute | null;
349
+ source: BncrExplicitTargetSource | null;
350
+ } {
351
+ let route: BncrRoute | null = null;
352
+ let source: BncrExplicitTargetSource | null = null;
353
+
354
+ const strict = parseStrictBncrSessionKey(raw);
355
+ if (strict?.route) {
356
+ route = strict.route;
357
+ source = 'strict-session-key';
382
358
  }
383
359
 
384
- if (!finalRoute) {
385
- const strict = parseStrictBncrSessionKey(raw);
386
- if (strict?.route) {
387
- finalRoute = strict.route;
360
+ if (!route) {
361
+ const displayRoute = parseRouteFromDisplayScope(raw);
362
+ if (displayRoute) {
363
+ route = displayRoute;
364
+ source = 'display-scope';
388
365
  }
389
366
  }
390
367
 
391
- if (!finalRoute) {
368
+ if (!route) {
392
369
  const legacy = parseLegacySessionKey(raw);
393
370
  if (legacy?.route) {
394
- finalRoute = legacy.route;
371
+ route = legacy.route;
372
+ source = legacy.source === 'hex' ? 'hex-scope' : 'legacy-session-key';
395
373
  }
396
374
  }
397
375
 
398
- if (!finalRoute) {
399
- const displayRoute = parseRouteFromDisplayScope(raw);
400
- if (displayRoute) {
401
- finalRoute = displayRoute;
376
+ if (!route) {
377
+ const hexRoute = parseRouteFromHexScope(raw);
378
+ if (hexRoute) {
379
+ route = hexRoute;
380
+ source = 'hex-scope';
402
381
  }
403
382
  }
404
383
 
405
- if (!finalRoute) {
384
+ if (!route) {
406
385
  const scopedRoute = parseRouteFromScope(raw);
407
386
  if (scopedRoute) {
408
- finalRoute = scopedRoute;
387
+ route = scopedRoute;
388
+ source = 'route-scope';
409
389
  }
410
390
  }
411
391
 
412
- if (!finalRoute && route) {
413
- finalRoute = route;
414
- }
392
+ return { route, source };
393
+ }
415
394
 
416
- if (!finalRoute) return null;
417
- return buildCanonicalBncrSessionKey(finalRoute, canonicalAgentId);
395
+ export function parseExplicitTarget(input: string, options?: { canonicalAgentId?: string | null }) {
396
+ const raw = asTargetString(input).trim();
397
+ if (!raw) return null;
398
+
399
+ const canonicalAgentId = asTargetString(options?.canonicalAgentId).trim() || undefined;
400
+ const { route, source } = resolveExplicitTargetRoute(raw);
401
+ if (!route || !source) return null;
402
+
403
+ const displayScope = formatDisplayScope(route);
404
+ return buildExplicitTargetResult({
405
+ raw,
406
+ source,
407
+ route,
408
+ displayScope,
409
+ ...(canonicalAgentId
410
+ ? { canonicalSessionKey: buildCanonicalBncrSessionKey(route, canonicalAgentId) }
411
+ : {}),
412
+ });
413
+ }
414
+
415
+ export function normalizeTaskKey(input: unknown): string | null {
416
+ const raw = asTargetString(input).trim().toLowerCase();
417
+ if (!raw) return null;
418
+ const normalized = raw
419
+ .replace(/[^a-z0-9_-]+/g, '-')
420
+ .replace(/^-+|-+$/g, '')
421
+ .slice(0, 32);
422
+ return normalized || null;
418
423
  }
419
424
 
420
425
  export function extractInlineTaskKey(text: string): { taskKey: string | null; text: string } {
421
- const raw = asString(text);
426
+ const raw = asTargetString(text);
422
427
  if (!raw) return { taskKey: null, text: '' };
423
428
 
424
429
  const tagged = raw.match(
@@ -427,7 +432,7 @@ export function extractInlineTaskKey(text: string): { taskKey: string | null; te
427
432
  if (tagged) {
428
433
  return {
429
434
  taskKey: normalizeTaskKey(tagged[1]),
430
- text: asString(tagged[2]),
435
+ text: asTargetString(tagged[2]),
431
436
  };
432
437
  }
433
438
 
@@ -435,7 +440,7 @@ export function extractInlineTaskKey(text: string): { taskKey: string | null; te
435
440
  if (spaced) {
436
441
  return {
437
442
  taskKey: normalizeTaskKey(spaced[1]),
438
- text: asString(spaced[2]),
443
+ text: asTargetString(spaced[2]),
439
444
  };
440
445
  }
441
446
 
@@ -443,15 +448,21 @@ export function extractInlineTaskKey(text: string): { taskKey: string | null; te
443
448
  }
444
449
 
445
450
  export function withTaskSessionKey(sessionKey: string, taskKey?: string | null): string {
446
- const base = asString(sessionKey).trim();
451
+ const base = asTargetString(sessionKey).trim();
447
452
  const tk = normalizeTaskKey(taskKey);
448
453
  if (!base || !tk) return base;
449
454
  if (/:task:[a-z0-9_-]+(?:$|:)/i.test(base)) return base;
450
455
  return `${base}:task:${tk}`;
451
456
  }
452
457
 
453
- export function buildFallbackSessionKey(route: BncrRoute, canonicalAgentId: string): string {
454
- return buildCanonicalBncrSessionKey(route, canonicalAgentId);
458
+ export function formatTargetDisplay(
459
+ input: BncrRoute | BncrExplicitTarget | null | undefined,
460
+ ): string {
461
+ if (!input) return '';
462
+ const routeInput = input && typeof input === 'object' ? (input as RouteInputLike) : null;
463
+ const route = parseRouteLike(routeInput?.route) || parseRouteLike(input);
464
+ if (!route) return '';
465
+ return formatDisplayScope(route);
455
466
  }
456
467
 
457
468
  export function routeKey(accountId: string, route: BncrRoute): string {