@xmoxmo/bncr 0.5.5 → 0.5.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 (78) hide show
  1. package/dist/index.js +1 -9
  2. package/index.ts +1 -9
  3. package/package.json +1 -1
  4. package/scripts/check-pack.mjs +0 -6
  5. package/scripts/selfcheck.mjs +0 -7
  6. package/src/channel.ts +114 -37
  7. package/src/core/accounts.ts +1 -6
  8. package/src/core/connection-reachability.ts +1 -5
  9. package/src/core/dead-letter-diagnostics.ts +4 -10
  10. package/src/core/outbox-entry-builders.ts +38 -15
  11. package/src/core/outbox-file-transfer-failure.ts +5 -0
  12. package/src/core/outbox-file-transfer-guards.ts +6 -0
  13. package/src/core/outbox-file-transfer-prep.ts +8 -5
  14. package/src/core/outbox-file-transfer-success.ts +5 -0
  15. package/src/core/outbox-queue.ts +1 -7
  16. package/src/core/outbox-summary.ts +15 -27
  17. package/src/core/outbox-text-push-prep.ts +1 -36
  18. package/src/core/permissions.ts +1 -9
  19. package/src/core/persisted-outbox-entry.ts +54 -11
  20. package/src/core/policy.ts +1 -11
  21. package/src/core/register-trace.ts +2 -5
  22. package/src/core/status-meta.ts +1 -9
  23. package/src/core/status.ts +1 -21
  24. package/src/core/targets.ts +52 -70
  25. package/src/core/types.ts +1 -17
  26. package/src/core/value-sanitize.ts +38 -1
  27. package/src/messaging/inbound/commands.ts +2 -24
  28. package/src/messaging/inbound/dispatch-prep.ts +1 -1
  29. package/src/messaging/inbound/gate.ts +2 -31
  30. package/src/messaging/inbound/group-history.ts +0 -75
  31. package/src/messaging/inbound/native-command.ts +7 -3
  32. package/src/messaging/inbound/parse.ts +1 -31
  33. package/src/messaging/inbound/scene-admin.ts +181 -5
  34. package/src/messaging/outbound/diagnostics.ts +1 -6
  35. package/src/messaging/outbound/durable-queue-adapter.ts +17 -11
  36. package/src/messaging/outbound/marker-parser.ts +188 -0
  37. package/src/messaging/outbound/media-dedupe.ts +2 -6
  38. package/src/messaging/outbound/media.ts +6 -20
  39. package/src/messaging/outbound/normalize-outbound-send.ts +327 -0
  40. package/src/messaging/outbound/queue-selectors.ts +1 -5
  41. package/src/messaging/outbound/reply-enqueue-media.ts +4 -0
  42. package/src/messaging/outbound/reply-enqueue.ts +130 -57
  43. package/src/messaging/outbound/session-route.ts +6 -11
  44. package/src/messaging/outbound/target-resolver.ts +1 -6
  45. package/src/openclaw/routing-runtime.ts +0 -11
  46. package/src/openclaw/sdk-helpers.ts +0 -2
  47. package/src/plugin/bridge-file-transfer-push-facade.ts +8 -6
  48. package/src/plugin/bridge-media-facade.ts +1 -1
  49. package/src/plugin/channel-components.ts +0 -13
  50. package/src/plugin/channel-plugin-surface-group.ts +57 -40
  51. package/src/plugin/channel-runtime-builders-delivery.ts +3 -0
  52. package/src/plugin/channel-runtime-types.ts +10 -24
  53. package/src/plugin/channel-send.ts +241 -154
  54. package/src/plugin/channel-utils.ts +8 -29
  55. package/src/plugin/connection-handlers-helpers.ts +0 -15
  56. package/src/plugin/connection-handlers.ts +6 -5
  57. package/src/plugin/file-inbound-abort.ts +5 -0
  58. package/src/plugin/file-inbound-chunk.ts +5 -0
  59. package/src/plugin/file-inbound-complete.ts +5 -0
  60. package/src/plugin/file-transfer-orchestrator.ts +105 -3
  61. package/src/plugin/file-transfer-setup.ts +1 -1
  62. package/src/plugin/media-dedupe-runtime.ts +1 -6
  63. package/src/plugin/media-orchestrators-runtime-group.ts +2 -0
  64. package/src/plugin/message-send.ts +7 -8
  65. package/src/plugin/outbound.ts +6 -22
  66. package/src/plugin/outbox-drain-loop.ts +2 -2
  67. package/src/plugin/outbox-file-push-flow.ts +7 -7
  68. package/src/plugin/outbox-text-push-flow.ts +1 -1
  69. package/src/plugin/target-runtime.ts +1 -6
  70. package/src/runtime/outbound-ack-timeout.ts +1 -8
  71. package/src/runtime/outbox-transitions.ts +1 -4
  72. package/src/runtime/status-worker.ts +1 -9
  73. package/src/messaging/outbound/build-send-action.ts +0 -130
  74. package/src/messaging/outbound/durable-message-adapter.ts +0 -121
  75. package/src/messaging/outbound/send-params.ts +0 -81
  76. package/src/messaging/outbound/send.ts +0 -121
  77. package/src/openclaw/ingress-runtime.ts +0 -25
  78. package/src/plugin/channel-send-runtime-group.ts +0 -37
@@ -1,26 +1,29 @@
1
1
  import type { BncrConnection, OutboxEntry } from './types.ts';
2
2
 
3
- export function prepareFileTransferRouteSelection(args: {
3
+ type PushRouteSelectionArgs<C> = {
4
4
  entry: OutboxEntry;
5
5
  owner: BncrConnection | null;
6
6
  resolvePushConnIds: (accountId: string) => Iterable<string>;
7
7
  resolveRecentInboundConnIds: (accountId: string) => Iterable<string>;
8
8
  hasRecentInboundReachability: (accountId: string) => boolean;
9
9
  isRevalidatedAttemptedConn: (connId: string) => boolean;
10
- selectOutboxFileTransferRouteCandidates: (args: {
10
+ selectRouteCandidates: (args: {
11
11
  routeCandidates: Iterable<string>;
12
12
  attemptedConnIds: string[];
13
13
  recentInboundConnIds: Iterable<string>;
14
14
  ownerConnId?: string;
15
15
  recentInboundReachable: boolean;
16
16
  isRevalidatedAttemptedConn: (connId: string) => boolean;
17
- }) => { connIds: Iterable<string>; recentInboundReachable: boolean; routeReason: string };
18
- }) {
17
+ }) => C;
18
+ };
19
+
20
+ /** Shared route selection prep for both text and file push flows. */
21
+ export function prepareRouteSelection<C>(args: PushRouteSelectionArgs<C>): C {
19
22
  const attemptedConnIds = Array.isArray(args.entry.routeAttemptConnIds)
20
23
  ? args.entry.routeAttemptConnIds.filter((v): v is string => typeof v === 'string' && !!v)
21
24
  : [];
22
25
 
23
- return args.selectOutboxFileTransferRouteCandidates({
26
+ return args.selectRouteCandidates({
24
27
  routeCandidates: args.resolvePushConnIds(args.entry.accountId),
25
28
  attemptedConnIds,
26
29
  recentInboundConnIds: args.resolveRecentInboundConnIds(args.entry.accountId),
@@ -1,3 +1,8 @@
1
+ /*
2
+ * Intentionally separate from outbox-text-push-success.ts.
3
+ * File transfer success uses kind:'file-transfer' tag and different
4
+ * argument shapes (owner vs connId). Separate callers, separate files.
5
+ */
1
6
  import { buildPushBroadcastPayload, buildPushRouteSelectArgs } from './outbox-push-args.ts';
2
7
  import type { BncrConnection, OutboxEntry } from './types.ts';
3
8
 
@@ -50,13 +50,7 @@ export function collectDueOutboxEntries(args: {
50
50
  nextAttemptAt: args.now + args.backoffMs(nextAttempt),
51
51
  };
52
52
  updatedEntries.push(updatedEntry);
53
- duePayloads.push({
54
- ...updatedEntry.payload,
55
- _meta: {
56
- retryCount: updatedEntry.retryCount,
57
- nextAttemptAt: updatedEntry.nextAttemptAt,
58
- },
59
- });
53
+ duePayloads.push(updatedEntry.payload);
60
54
 
61
55
  if (duePayloads.length >= args.maxBatch) break;
62
56
  }
@@ -1,23 +1,16 @@
1
1
  import type { OutboxEntry } from './types.ts';
2
2
 
3
- type OutboxSummaryMeta = {
4
- kind?: string;
5
- asVoice?: boolean;
6
- audioAsVoice?: boolean;
7
- type?: unknown;
8
- mediaUrl?: unknown;
9
- text?: unknown;
10
- };
11
-
12
3
  type OutboxSummaryMessage = {
13
4
  type?: unknown;
14
5
  msg?: unknown;
6
+ mediaUrl?: unknown;
7
+ audioAsVoice?: boolean;
8
+ asVoice?: boolean;
15
9
  };
16
10
 
17
11
  type OutboxSummaryPayload = {
18
12
  type?: unknown;
19
13
  message?: OutboxSummaryMessage;
20
- _meta?: OutboxSummaryMeta;
21
14
  };
22
15
 
23
16
  function inferMediaTypeFromUrl(raw: string): 'image' | 'video' | 'audio' | 'file' {
@@ -47,27 +40,22 @@ function summarizeOutboxType(
47
40
  ) {
48
41
  const directType = asString(msg.type || '');
49
42
  if (directType) return directType;
50
-
51
- const meta = payload?._meta || {};
52
- if (meta.kind === 'file-transfer') {
53
- if (meta.asVoice === true || meta.audioAsVoice === true) return 'voice';
54
- const hintedType = asString(meta.type || '').trim();
55
- if (hintedType) return hintedType;
56
- return inferMediaTypeFromUrl(asString(meta.mediaUrl || ''));
43
+ // Infer type from mediaUrl when no direct type is set
44
+ const mediaUrl = asString(msg.mediaUrl || '');
45
+ if (mediaUrl) {
46
+ if (msg.audioAsVoice === true || msg.asVoice === true) return 'voice';
47
+ const inferred = inferMediaTypeFromUrl(mediaUrl);
48
+ if (inferred) return inferred;
57
49
  }
58
-
59
50
  return asString(payload?.type || 'unknown');
60
51
  }
61
52
 
62
- function summarizeOutboxText(
63
- payload: OutboxSummaryPayload,
64
- msg: OutboxSummaryMessage,
65
- asString: (value: unknown) => string,
66
- ) {
67
- const text = asString(msg.msg || payload?._meta?.text || '').trim();
53
+ function summarizeOutboxText(msg: OutboxSummaryMessage, asString: (value: unknown) => string) {
54
+ const text = asString(msg.msg || '').trim();
68
55
  if (text) return text;
69
- const meta = payload?._meta || {};
70
- if (meta.kind === 'file-transfer') return filenameFromUrl(asString(meta.mediaUrl || ''));
56
+ // Fall back to filename from mediaUrl when no caption is set
57
+ const mediaUrl = asString(msg.mediaUrl || '');
58
+ if (mediaUrl) return filenameFromUrl(mediaUrl);
71
59
  return '';
72
60
  }
73
61
 
@@ -80,7 +68,7 @@ export function summarizeOutboxEntry(args: {
80
68
  const payload = args.entry.payload as OutboxSummaryPayload;
81
69
  const msg = payload?.message || {};
82
70
  const type = summarizeOutboxType(payload, msg, args.asString);
83
- const text = summarizeOutboxText(payload, msg, args.asString);
71
+ const text = summarizeOutboxText(msg, args.asString);
84
72
  const preview = args.summarizeTextPreview(text);
85
73
  return [type, args.formatDisplayScope(args.entry.route), preview].join('|');
86
74
  }
@@ -1,36 +1 @@
1
- import type { BncrConnection, OutboxEntry } from './types.ts';
2
-
3
- export function prepareTextPushRouteSelection(args: {
4
- entry: OutboxEntry;
5
- owner: BncrConnection | null;
6
- resolvePushConnIds: (accountId: string) => Iterable<string>;
7
- resolveRecentInboundConnIds: (accountId: string) => Iterable<string>;
8
- hasRecentInboundReachability: (accountId: string) => boolean;
9
- isRevalidatedAttemptedConn: (connId: string) => boolean;
10
- selectOutboxRouteCandidates: (args: {
11
- routeCandidates: Iterable<string>;
12
- attemptedConnIds: string[];
13
- recentInboundConnIds: Iterable<string>;
14
- ownerConnId?: string;
15
- recentInboundReachable: boolean;
16
- isRevalidatedAttemptedConn: (connId: string) => boolean;
17
- }) => {
18
- connIds: Iterable<string>;
19
- recentInboundReachable: boolean;
20
- routeReason: string;
21
- ownerConnId?: string;
22
- };
23
- }) {
24
- const attemptedConnIds = Array.isArray(args.entry.routeAttemptConnIds)
25
- ? args.entry.routeAttemptConnIds.filter((v): v is string => typeof v === 'string' && !!v)
26
- : [];
27
-
28
- return args.selectOutboxRouteCandidates({
29
- routeCandidates: args.resolvePushConnIds(args.entry.accountId),
30
- attemptedConnIds,
31
- recentInboundConnIds: args.resolveRecentInboundConnIds(args.entry.accountId),
32
- ownerConnId: args.owner?.connId,
33
- recentInboundReachable: args.hasRecentInboundReachability(args.entry.accountId),
34
- isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
35
- });
36
- }
1
+ export { prepareRouteSelection as prepareTextPushRouteSelection } from './outbox-file-transfer-prep.ts';
@@ -1,8 +1,4 @@
1
- function asString(v: unknown, fallback = ''): string {
2
- if (typeof v === 'string') return v;
3
- if (v == null) return fallback;
4
- return String(v);
5
- }
1
+ import { asString } from '../core/value-sanitize.ts';
6
2
 
7
3
  type BncrElevatedConfigRoot = {
8
4
  tools?: {
@@ -14,21 +10,18 @@ type BncrElevatedConfigRoot = {
14
10
  };
15
11
  };
16
12
  };
17
-
18
13
  export function getBncrElevatedConfig(rootCfg: BncrElevatedConfigRoot | null | undefined) {
19
14
  const elevated = rootCfg?.tools?.elevated || {};
20
15
  const allowFrom = elevated?.allowFrom || {};
21
16
  const bncrRules = Array.isArray(allowFrom?.bncr)
22
17
  ? allowFrom.bncr.map((x: unknown) => asString(x).trim()).filter(Boolean)
23
18
  : [];
24
-
25
19
  return {
26
20
  enabled: elevated?.enabled === true,
27
21
  bncrAllowed: bncrRules.length > 0,
28
22
  bncrRules,
29
23
  };
30
24
  }
31
-
32
25
  export function buildBncrPermissionSummary(rootCfg: BncrElevatedConfigRoot | null | undefined) {
33
26
  const elevated = getBncrElevatedConfig(rootCfg);
34
27
  return {
@@ -40,5 +33,4 @@ export function buildBncrPermissionSummary(rootCfg: BncrElevatedConfigRoot | nul
40
33
  : 'bncr elevated not explicitly allowed',
41
34
  };
42
35
  }
43
-
44
36
  export type BncrPermissionSummary = ReturnType<typeof buildBncrPermissionSummary>;
@@ -1,22 +1,12 @@
1
1
  import { normalizeAccountId } from './accounts.ts';
2
2
  import { normalizeStoredSessionKey, parseRouteLike } from './targets.ts';
3
3
  import type { OutboxEntry } from './types.ts';
4
+ import { asString, finiteNumberOr } from './value-sanitize.ts';
4
5
 
5
6
  type PersistedOutboxEntryInput = Partial<OutboxEntry> & {
6
7
  payload?: Record<string, unknown>;
7
8
  };
8
9
 
9
- function asString(v: unknown, fallback = ''): string {
10
- if (typeof v === 'string') return v;
11
- if (v == null) return fallback;
12
- return String(v);
13
- }
14
-
15
- function finiteNumberOr(value: unknown, fallback: number): number {
16
- const n = Number(value);
17
- return Number.isFinite(n) ? n : fallback;
18
- }
19
-
20
10
  function optionalFiniteNumber(value: unknown): number | undefined {
21
11
  if (value == null || value === '') return undefined;
22
12
  const n = Number(value);
@@ -43,6 +33,59 @@ export function normalizePersistedOutboxEntry(args: {
43
33
  payload.groupId = route.groupId;
44
34
  payload.userId = route.userId;
45
35
 
36
+ // Migrate old _meta format entries to the new message format.
37
+ // Old entries persisted before the _meta→message refactor carry media
38
+ // metadata in payload._meta. New code reads from payload.message.
39
+ const oldMeta = payload._meta as Record<string, unknown> | undefined;
40
+ if (oldMeta && !payload.message) {
41
+ const oldKind = asString(oldMeta.kind);
42
+ if (oldKind === 'file-transfer') {
43
+ // Move replyToId from _meta to payload top level
44
+ const replyToId = asString(oldMeta.replyToId || '').trim() || undefined;
45
+ if (replyToId) payload.replyToId = replyToId;
46
+
47
+ // Build new message object from old _meta fields
48
+ const msg: Record<string, unknown> = {
49
+ type: (oldMeta.type as string) || 'file',
50
+ msg: oldMeta.text || '',
51
+ mediaUrl: oldMeta.mediaUrl,
52
+ mediaLocalRoots: oldMeta.mediaLocalRoots,
53
+ asVoice: oldMeta.asVoice === true,
54
+ audioAsVoice: oldMeta.audioAsVoice === true,
55
+ downloadMedia: oldMeta.downloadMedia === true,
56
+ kind: oldMeta.messageKind,
57
+ transferMode: 'media',
58
+ path: '',
59
+ base64: '',
60
+ fileName: '',
61
+ };
62
+ // Strip consumed _meta keys, pass through remaining as extra
63
+ const consumed = new Set([
64
+ 'kind',
65
+ 'mediaUrl',
66
+ 'mediaLocalRoots',
67
+ 'text',
68
+ 'asVoice',
69
+ 'audioAsVoice',
70
+ 'replyToId',
71
+ 'finalEvent',
72
+ 'type',
73
+ 'downloadMedia',
74
+ 'messageKind',
75
+ 'retryCount',
76
+ 'nextAttemptAt',
77
+ ]);
78
+ for (const [key, value] of Object.entries(oldMeta)) {
79
+ if (!consumed.has(key)) {
80
+ msg[key] = value;
81
+ }
82
+ }
83
+ payload.message = msg;
84
+ }
85
+ // Remove old _meta after migration
86
+ delete payload._meta;
87
+ }
88
+
46
89
  return {
47
90
  ...entry,
48
91
  messageId: asString(entry.messageId).trim(),
@@ -1,8 +1,4 @@
1
- function asString(v: unknown, fallback = ''): string {
2
- if (typeof v === 'string') return v;
3
- if (v == null) return fallback;
4
- return String(v);
5
- }
1
+ import { asBoolean, asString } from './value-sanitize.ts';
6
2
 
7
3
  export type BncrChannelPolicyConfig = {
8
4
  enabled?: boolean;
@@ -21,12 +17,6 @@ function asList(v: unknown): string[] {
21
17
  return v.map((x) => asString(x).trim()).filter(Boolean);
22
18
  }
23
19
 
24
- function asBoolean(v: unknown, fallback = false): boolean {
25
- if (typeof v === 'boolean') return v;
26
- if (v == null) return fallback;
27
- return String(v).trim().toLowerCase() === 'true';
28
- }
29
-
30
20
  function normalizeDmPolicy(value: unknown): BncrDmPolicy {
31
21
  const normalized = asString(value || 'open')
32
22
  .trim()
@@ -1,9 +1,6 @@
1
- const DEFAULT_REGISTER_WARMUP_WINDOW_MS = 30_000;
1
+ import { finiteNumberOr } from './value-sanitize.ts';
2
2
 
3
- function finiteNumberOr(value: unknown, fallback: number): number {
4
- const n = Number(value);
5
- return Number.isFinite(n) ? n : fallback;
6
- }
3
+ const DEFAULT_REGISTER_WARMUP_WINDOW_MS = 30_000;
7
4
 
8
5
  export type RegisterTraceEntry = {
9
6
  ts: number;
@@ -1,4 +1,5 @@
1
1
  import type { PendingAdmission } from './types.ts';
2
+ import { finiteNumberOrNull, now } from './value-sanitize.ts';
2
3
 
3
4
  type RuntimeStatusMetaDisplayInput = {
4
5
  pendingAdmissions?: PendingAdmission[];
@@ -8,15 +9,6 @@ type RuntimeStatusMetaDisplayInput = {
8
9
  lastOutboundAt?: number | null;
9
10
  };
10
11
 
11
- function now() {
12
- return Date.now();
13
- }
14
-
15
- function finiteNumberOrNull(value: unknown): number | null {
16
- const n = Number(value);
17
- return Number.isFinite(n) ? n : null;
18
- }
19
-
20
12
  function formatPendingAdmissionScope(route: unknown): string | null {
21
13
  if (!route || typeof route !== 'object') return null;
22
14
  const record = route as Record<string, unknown>;
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { buildRuntimeStatusMetaDisplay } from './status-meta.ts';
4
4
  import type { BncrDiagnosticsSummary, BncrRuntimeLastSession, PendingAdmission } from './types.ts';
5
+ import { finiteNumberOr, nonNegativeFiniteNumberOr, now } from './value-sanitize.ts';
5
6
 
6
7
  type RuntimeStatusInput = {
7
8
  accountId: string;
@@ -27,19 +28,6 @@ type RuntimeStatusInput = {
27
28
  channelRoot?: string;
28
29
  };
29
30
 
30
- function now() {
31
- return Date.now();
32
- }
33
-
34
- function finiteNumberOr(value: unknown, fallback: number): number {
35
- const n = Number(value);
36
- return Number.isFinite(n) ? n : fallback;
37
- }
38
-
39
- function nonNegativeFiniteNumberOr(value: unknown, fallback: number): number {
40
- return Math.max(0, finiteNumberOr(value, fallback));
41
- }
42
-
43
31
  export function buildIntegratedDiagnostics(input: RuntimeStatusInput): BncrDiagnosticsSummary {
44
32
  const root = input.channelRoot || path.join(process.cwd(), 'plugins', 'bncr');
45
33
  const pluginIndexExists = fs.existsSync(path.join(root, 'index.ts'));
@@ -230,11 +218,3 @@ export function buildAccountStatusSnapshot(input: {
230
218
  diagnostics,
231
219
  };
232
220
  }
233
-
234
- export function buildChannelSummaryFromRuntime(input: RuntimeStatusInput) {
235
- const headline = buildStatusHeadlineFromRuntime(input);
236
- return {
237
- linked: input.connected,
238
- self: { e164: headline },
239
- };
240
- }