@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,13 +1,23 @@
1
1
  import type { BncrConnection, OutboxEntry } from './types.ts';
2
2
 
3
+ function finiteNumberOr(value: unknown, fallback: number): number {
4
+ const n = Number(value);
5
+ return Number.isFinite(n) ? n : fallback;
6
+ }
7
+
3
8
  export function hasRecentInboundReachability(args: {
4
9
  now: number;
5
10
  windowMs: number;
6
11
  lastInboundAt: number;
7
12
  lastActivityAt: number;
8
13
  }) {
9
- const lastReachableAt = Math.max(args.lastInboundAt, args.lastActivityAt);
10
- return lastReachableAt > 0 && args.now - lastReachableAt <= args.windowMs;
14
+ const nowMs = finiteNumberOr(args.now, 0);
15
+ const windowMs = finiteNumberOr(args.windowMs, 0);
16
+ const lastReachableAt = Math.max(
17
+ finiteNumberOr(args.lastInboundAt, 0),
18
+ finiteNumberOr(args.lastActivityAt, 0),
19
+ );
20
+ return nowMs > 0 && windowMs > 0 && lastReachableAt > 0 && nowMs - lastReachableAt <= windowMs;
11
21
  }
12
22
 
13
23
  export function resolveRecentInboundConnIds(args: {
@@ -20,10 +30,16 @@ export function resolveRecentInboundConnIds(args: {
20
30
  const connIds = new Set<string>();
21
31
  if (!args.recentInboundReachable) return connIds;
22
32
 
33
+ const nowMs = finiteNumberOr(args.now, 0);
34
+ const connectTtlMs = finiteNumberOr(args.connectTtlMs, 0);
35
+ if (!nowMs || !connectTtlMs) return connIds;
36
+
23
37
  for (const c of args.connections) {
24
38
  if (c.accountId !== args.accountId) continue;
25
39
  if (!c.connId) continue;
26
- if (args.now - c.lastSeenAt > args.connectTtlMs * 2) continue;
40
+ const lastSeenAt = finiteNumberOr(c.lastSeenAt, 0);
41
+ if (!lastSeenAt) continue;
42
+ if (nowMs - lastSeenAt > connectTtlMs * 2) continue;
27
43
  connIds.add(c.connId);
28
44
  }
29
45
 
@@ -60,11 +76,16 @@ export function hasAlternativeLiveConnection(args: {
60
76
  }) {
61
77
  const currentConn = String(args.currentConnId || '').trim();
62
78
  const currentClient = String(args.currentClientId || '').trim() || undefined;
79
+ const nowMs = finiteNumberOr(args.now, 0);
80
+ const connectTtlMs = finiteNumberOr(args.connectTtlMs, 0);
81
+ if (!nowMs || !connectTtlMs) return false;
63
82
 
64
83
  for (const conn of args.connections) {
65
84
  if (conn.accountId !== args.accountId) continue;
66
85
  if (!conn.connId) continue;
67
- if (args.now - conn.lastSeenAt > args.connectTtlMs) continue;
86
+ const lastSeenAt = finiteNumberOr(conn.lastSeenAt, 0);
87
+ if (!lastSeenAt) continue;
88
+ if (nowMs - lastSeenAt > connectTtlMs) continue;
68
89
  const sameConn = !!currentConn && conn.connId === currentConn;
69
90
  const sameClient = !currentConn && !!currentClient && conn.clientId === currentClient;
70
91
  if (sameConn || sameClient) continue;
@@ -93,27 +114,33 @@ export function getRevalidatedAttemptReason(args: {
93
114
  const targetConnId = String(args.connId || '').trim();
94
115
  if (!targetConnId) return null;
95
116
 
96
- const lastAttemptAt = Number(args.entry.lastAttemptAt || 0);
117
+ const nowMs = finiteNumberOr(args.now, 0);
118
+ const connectTtlMs = finiteNumberOr(args.connectTtlMs, 0);
119
+ if (!nowMs || !connectTtlMs) return null;
120
+
121
+ const lastAttemptAt = finiteNumberOr(args.entry.lastAttemptAt, 0);
97
122
  for (const rawConn of args.connections) {
98
123
  const conn = rawConn as ReachableConnection;
99
124
  if (conn.accountId !== args.accountId) continue;
100
125
  if (conn.connId !== targetConnId) continue;
101
- if (args.now - conn.lastSeenAt > args.connectTtlMs) continue;
126
+ const lastSeenAt = finiteNumberOr(conn.lastSeenAt, 0);
127
+ if (!lastSeenAt) continue;
128
+ if (nowMs - lastSeenAt > connectTtlMs) continue;
102
129
  if (conn.inboundOnly === true) continue;
103
130
 
104
- const preferredForOutboundUntil = Number(conn.preferredForOutboundUntil || 0);
105
- const outboundReadyUntil = Number(conn.outboundReadyUntil || 0);
106
- const lastAckOkAt = Number(conn.lastAckOkAt || 0);
107
- const lastPushTimeoutAt = Number(conn.lastPushTimeoutAt || 0);
131
+ const preferredForOutboundUntil = finiteNumberOr(conn.preferredForOutboundUntil, 0);
132
+ const outboundReadyUntil = finiteNumberOr(conn.outboundReadyUntil, 0);
133
+ const lastAckOkAt = finiteNumberOr(conn.lastAckOkAt, 0);
134
+ const lastPushTimeoutAt = finiteNumberOr(conn.lastPushTimeoutAt, 0);
108
135
 
109
- const revalidatedByPreferred = preferredForOutboundUntil > args.now;
110
- const revalidatedByReady = outboundReadyUntil > args.now;
136
+ const revalidatedByPreferred = preferredForOutboundUntil > nowMs;
137
+ const revalidatedByReady = outboundReadyUntil > nowMs;
111
138
  const revalidatedByAck = lastAckOkAt > 0 && lastAckOkAt > lastAttemptAt;
112
139
  const revalidatedByFreshReachability =
113
140
  args.recentInboundReachable &&
114
141
  lastPushTimeoutAt > 0 &&
115
142
  lastPushTimeoutAt <= lastAttemptAt &&
116
- conn.lastSeenAt > lastPushTimeoutAt;
143
+ lastSeenAt > lastPushTimeoutAt;
117
144
 
118
145
  if (!revalidatedByPreferred && !revalidatedByReady && !revalidatedByAck && !revalidatedByFreshReachability) {
119
146
  return null;
@@ -132,7 +159,7 @@ export function getRevalidatedAttemptReason(args: {
132
159
  lastPushTimeoutAt: lastPushTimeoutAt || null,
133
160
  outboundReadyUntil: outboundReadyUntil || null,
134
161
  preferredForOutboundUntil: preferredForOutboundUntil || null,
135
- lastSeenAt: conn.lastSeenAt,
162
+ lastSeenAt,
136
163
  recentInboundReachable: args.recentInboundReachable,
137
164
  };
138
165
  }
@@ -1,6 +1,11 @@
1
1
  import { buildBncrPermissionSummary } from './permissions.ts';
2
2
  import { probeBncrAccount } from './probe.ts';
3
3
 
4
+ function finiteNumberOr(value: unknown, fallback: number): number {
5
+ const n = Number(value);
6
+ return Number.isFinite(n) ? n : fallback;
7
+ }
8
+
4
9
  type DiagnosticsPayloadArgs = {
5
10
  cfg: any;
6
11
  channelId: string;
@@ -21,8 +26,8 @@ export function buildDiagnosticsPayload(args: DiagnosticsPayloadArgs) {
21
26
  const probe = probeBncrAccount({
22
27
  accountId: args.accountId,
23
28
  connected: Boolean(args.runtime?.connected),
24
- pending: Number(args.runtime?.meta?.pending ?? 0),
25
- deadLetter: Number(args.runtime?.meta?.deadLetter ?? 0),
29
+ pending: finiteNumberOr(args.runtime?.meta?.pending, 0),
30
+ deadLetter: finiteNumberOr(args.runtime?.meta?.deadLetter, 0),
26
31
  activeConnections: args.activeConnections,
27
32
  invalidOutboxSessionKeys: args.invalidOutboxSessionKeys,
28
33
  legacyAccountResidue: args.legacyAccountResidue,
@@ -1,5 +1,10 @@
1
1
  import type { OutboxEntry } from './types.ts';
2
2
 
3
+ function finiteNumberOr(value: unknown, fallback: number): number {
4
+ const n = Number(value);
5
+ return Number.isFinite(n) ? n : fallback;
6
+ }
7
+
3
8
  type DownlinkHealthInput = {
4
9
  accountId: string;
5
10
  now: number;
@@ -17,13 +22,13 @@ export function buildDownlinkHealth(input: DownlinkHealthInput) {
17
22
  const pending = Array.from(input.outboxEntries).filter((v) => v.accountId === input.accountId);
18
23
  const pendingCount = pending.length;
19
24
  const oldestPendingCreatedAt = pending.length
20
- ? Math.min(...pending.map((entry) => Number(entry.createdAt || input.now)))
25
+ ? Math.min(...pending.map((entry) => finiteNumberOr(entry.createdAt, input.now)))
21
26
  : null;
22
27
  const oldestPendingAgeMs = oldestPendingCreatedAt
23
28
  ? Math.max(0, input.now - oldestPendingCreatedAt)
24
29
  : 0;
25
30
  const lastSignalAt =
26
- Math.max(Number(input.lastInboundAt || 0), Number(input.lastActivityAt || 0)) || null;
31
+ Math.max(finiteNumberOr(input.lastInboundAt, 0), finiteNumberOr(input.lastActivityAt, 0)) || null;
27
32
  const inboundHealthy = !!lastSignalAt && input.now - lastSignalAt <= 5 * 60 * 1000;
28
33
  const ackRecentlyHealthy =
29
34
  !!input.lastAckOkAt && input.now - input.lastAckOkAt <= 5 * 60 * 1000;
@@ -1,3 +1,4 @@
1
+ import { normalizeOutboundReplyToId } from '../messaging/outbound/reply-target-policy.ts';
1
2
  import type { BncrRoute, OutboxEntry } from './types.ts';
2
3
 
3
4
  export function buildFileTransferOutboxEntry(args: {
@@ -34,7 +35,7 @@ export function buildFileTransferOutboxEntry(args: {
34
35
  asVoice: args.asVoice === true,
35
36
  audioAsVoice: args.audioAsVoice === true,
36
37
  finalEvent: args.pushEvent,
37
- replyToId: args.replyToId,
38
+ replyToId: normalizeOutboundReplyToId({ kind: args.kind, replyToId: args.replyToId }) || undefined,
38
39
  messageKind: args.kind,
39
40
  },
40
41
  },
@@ -63,7 +64,7 @@ export function buildTextOutboxEntry(args: {
63
64
  messageId,
64
65
  idempotencyKey: messageId,
65
66
  sessionKey: args.sessionKey,
66
- replyToId: args.normalizeReplyToId(args.replyToId) || undefined,
67
+ replyToId: normalizeOutboundReplyToId({ kind: args.kind, replyToId: args.replyToId }) || undefined,
67
68
  message: {
68
69
  platform: args.route.platform,
69
70
  groupId: args.route.groupId,
@@ -25,3 +25,12 @@ export function resolveBncrChannelPolicy(channelCfg: any) {
25
25
  requireMention: asBoolean(channelCfg?.requireMention, false),
26
26
  };
27
27
  }
28
+
29
+ export function resolveBncrConfigWarnings(channelCfg: any): string[] {
30
+ const policy = resolveBncrChannelPolicy(channelCfg || {});
31
+ const warnings: string[] = [];
32
+ if (policy.requireMention) {
33
+ warnings.push('requireMention configured but not enforced yet');
34
+ }
35
+ return warnings;
36
+ }
@@ -1,5 +1,10 @@
1
1
  const DEFAULT_REGISTER_WARMUP_WINDOW_MS = 30_000;
2
2
 
3
+ function finiteNumberOr(value: unknown, fallback: number): number {
4
+ const n = Number(value);
5
+ return Number.isFinite(n) ? n : fallback;
6
+ }
7
+
3
8
  export type RegisterTraceEntry = {
4
9
  ts: number;
5
10
  bridgeId: string;
@@ -70,7 +75,7 @@ export function buildRegisterTraceSummary(args: {
70
75
  }): RegisterTraceSummary {
71
76
  const warmupWindowMs = Math.max(
72
77
  0,
73
- Number(args.warmupWindowMs ?? DEFAULT_REGISTER_WARMUP_WINDOW_MS) || 0,
78
+ finiteNumberOr(args.warmupWindowMs, DEFAULT_REGISTER_WARMUP_WINDOW_MS),
74
79
  );
75
80
  const buckets: Record<string, number> = {};
76
81
  let warmupCount = 0;
@@ -30,6 +30,11 @@ function now() {
30
30
  return Date.now();
31
31
  }
32
32
 
33
+ function finiteNumberOr(value: unknown, fallback: number): number {
34
+ const n = Number(value);
35
+ return Number.isFinite(n) ? n : fallback;
36
+ }
37
+
33
38
  function fmtAgo(ts?: number | null): string {
34
39
  if (!ts || !Number.isFinite(ts) || ts <= 0) return '-';
35
40
  const diff = Math.max(0, now() - ts);
@@ -151,8 +156,8 @@ export function buildAccountStatusSnapshot(input: {
151
156
  const rt = input.runtime || {};
152
157
  const meta = rt?.meta || {};
153
158
 
154
- const pending = Number(rt?.pending ?? meta.pending ?? 0);
155
- const deadLetter = Number(rt?.deadLetter ?? meta.deadLetter ?? 0);
159
+ const pending = finiteNumberOr(rt?.pending ?? meta.pending, 0);
160
+ const deadLetter = finiteNumberOr(rt?.deadLetter ?? meta.deadLetter, 0);
156
161
  const lastSessionKey = rt?.lastSessionKey ?? meta.lastSessionKey ?? null;
157
162
  const lastSessionScope = rt?.lastSessionScope ?? meta.lastSessionScope ?? null;
158
163
  const lastSessionAt = rt?.lastSessionAt ?? meta.lastSessionAt ?? null;
@@ -51,8 +51,17 @@ function parseRouteFromStandardDisplayScope(scope: string): BncrRoute | null {
51
51
  return null;
52
52
  }
53
53
 
54
- export function parseRouteFromDisplayScope(scope: string): BncrRoute | null {
54
+ function normalizeDisplayScopePrefix(scope: string): string {
55
55
  const raw = asString(scope).trim();
56
+ if (!raw) return '';
57
+ if (raw.startsWith('Bncr:')) return raw;
58
+ if (/^bncr[:-]/i.test(raw)) return raw;
59
+ if (!parseRouteFromStandardDisplayScope(raw)) return raw;
60
+ return `Bncr:${raw}`;
61
+ }
62
+
63
+ export function parseRouteFromDisplayScope(scope: string): BncrRoute | null {
64
+ const raw = normalizeDisplayScopePrefix(scope);
56
65
  if (!raw) return null;
57
66
 
58
67
  const payload = raw.match(/^Bncr:(.+)$/)?.[1];
package/src/core/types.ts CHANGED
@@ -52,6 +52,7 @@ export type OutboxEntry = {
52
52
  routeAttemptConnIds?: string[];
53
53
  routeAttemptRound?: number;
54
54
  fastReroutePending?: boolean;
55
+ awaitingRetryPush?: boolean;
55
56
  };
56
57
 
57
58
  export type BncrDiagnosticsSummary = {