@xmoxmo/bncr 0.3.6 → 0.3.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 (165) hide show
  1. package/README.md +5 -0
  2. package/dist/index.js +28 -5
  3. package/index.ts +55 -721
  4. package/openclaw.plugin.json +1 -0
  5. package/package.json +8 -4
  6. package/scripts/check-pack.mjs +93 -18
  7. package/scripts/check-register-drift.mjs +35 -13
  8. package/scripts/selfcheck.mjs +80 -11
  9. package/src/bootstrap/channel-plugin-runtime.ts +81 -0
  10. package/src/bootstrap/cli.ts +97 -0
  11. package/src/bootstrap/register-runtime-gateway.ts +129 -0
  12. package/src/bootstrap/register-runtime-helpers.ts +140 -0
  13. package/src/bootstrap/register-runtime-singleton.ts +137 -0
  14. package/src/bootstrap/register-runtime.ts +201 -0
  15. package/src/bootstrap/runtime-discovery.ts +187 -0
  16. package/src/bootstrap/runtime-loader.ts +54 -0
  17. package/src/channel.ts +1590 -4967
  18. package/src/core/accounts.ts +23 -4
  19. package/src/core/dead-letter-diagnostics.ts +37 -5
  20. package/src/core/diagnostics.ts +31 -15
  21. package/src/core/downlink-health.ts +3 -11
  22. package/src/core/extended-diagnostics.ts +78 -36
  23. package/src/core/file-transfer-payloads.ts +1 -1
  24. package/src/core/logging.ts +1 -0
  25. package/src/core/outbox-enqueue.ts +13 -2
  26. package/src/core/outbox-entry-builders.ts +2 -0
  27. package/src/core/outbox-summary.ts +75 -3
  28. package/src/core/permissions.ts +15 -2
  29. package/src/core/persisted-outbox-entry.ts +21 -6
  30. package/src/core/policy.ts +45 -4
  31. package/src/core/probe.ts +3 -15
  32. package/src/core/register-trace.ts +3 -3
  33. package/src/core/status.ts +43 -4
  34. package/src/core/targets.ts +216 -205
  35. package/src/core/types.ts +221 -0
  36. package/src/core/value-sanitize.ts +29 -0
  37. package/src/messaging/inbound/commands.ts +147 -172
  38. package/src/messaging/inbound/context-facts.ts +4 -2
  39. package/src/messaging/inbound/contracts.ts +70 -0
  40. package/src/messaging/inbound/dispatch-prep.ts +303 -0
  41. package/src/messaging/inbound/dispatch.ts +49 -462
  42. package/src/messaging/inbound/gate.ts +18 -5
  43. package/src/messaging/inbound/last-route.ts +10 -4
  44. package/src/messaging/inbound/media-url-download.ts +109 -0
  45. package/src/messaging/inbound/native-command-runtime.ts +225 -0
  46. package/src/messaging/inbound/parse.ts +2 -1
  47. package/src/messaging/inbound/remote-media.ts +49 -0
  48. package/src/messaging/inbound/reply-config.ts +16 -4
  49. package/src/messaging/inbound/reply-dispatch.ts +162 -0
  50. package/src/messaging/inbound/runtime-compat.ts +31 -10
  51. package/src/messaging/inbound/session-label.ts +15 -7
  52. package/src/messaging/inbound/turn-context.ts +131 -0
  53. package/src/messaging/outbound/actions.ts +24 -10
  54. package/src/messaging/outbound/diagnostics-debug-builders.ts +365 -0
  55. package/src/messaging/outbound/diagnostics.ts +31 -355
  56. package/src/messaging/outbound/durable-message-adapter.ts +20 -16
  57. package/src/messaging/outbound/durable-queue-adapter.ts +20 -7
  58. package/src/messaging/outbound/media.ts +24 -13
  59. package/src/messaging/outbound/reply-enqueue-media.ts +181 -0
  60. package/src/messaging/outbound/reply-enqueue.ts +46 -155
  61. package/src/messaging/outbound/send-params.ts +3 -0
  62. package/src/messaging/outbound/send.ts +19 -10
  63. package/src/messaging/outbound/session-route.ts +18 -3
  64. package/src/openclaw/channel-runtime-contracts.ts +76 -0
  65. package/src/openclaw/config-runtime.ts +13 -7
  66. package/src/openclaw/inbound-session-runtime.ts +7 -3
  67. package/src/openclaw/ingress-runtime.ts +17 -27
  68. package/src/openclaw/reply-runtime.ts +54 -59
  69. package/src/openclaw/routing-runtime.ts +35 -18
  70. package/src/openclaw/runtime-surface.ts +156 -12
  71. package/src/openclaw/sdk-helpers.ts +8 -1
  72. package/src/openclaw/session-route-runtime.ts +12 -12
  73. package/src/plugin/ack-outbox-runtime-group.ts +264 -0
  74. package/src/plugin/bridge-ack-facade.ts +137 -0
  75. package/src/plugin/bridge-connection-facade.ts +111 -0
  76. package/src/plugin/bridge-diagnostics-facade.ts +23 -0
  77. package/src/plugin/bridge-drain-facade.ts +98 -0
  78. package/src/plugin/bridge-extended-diagnostics-facade.ts +149 -0
  79. package/src/plugin/bridge-file-transfer-push-facade.ts +140 -0
  80. package/src/plugin/bridge-lifecycle.ts +156 -0
  81. package/src/plugin/bridge-media-facade.ts +241 -0
  82. package/src/plugin/bridge-outbox-facade.ts +182 -0
  83. package/src/plugin/bridge-runtime-helpers.ts +266 -0
  84. package/src/plugin/bridge-runtime-snapshots.ts +104 -0
  85. package/src/plugin/bridge-runtime-surface-facade.ts +8 -0
  86. package/src/plugin/bridge-status-facade.ts +76 -0
  87. package/src/plugin/bridge-status-worker-facade.ts +72 -0
  88. package/src/plugin/bridge-support-runtime.ts +137 -0
  89. package/src/plugin/bridge-surface-handlers-group.ts +242 -0
  90. package/src/plugin/bridge-surface-helpers.ts +28 -0
  91. package/src/plugin/capabilities.ts +1 -3
  92. package/src/plugin/channel-components.ts +289 -0
  93. package/src/plugin/channel-inbound-helpers.ts +149 -0
  94. package/src/plugin/channel-plugin-bridge-group.ts +129 -0
  95. package/src/plugin/channel-plugin-surface-group.ts +202 -0
  96. package/src/plugin/channel-runtime-builders-delivery.ts +513 -0
  97. package/src/plugin/channel-runtime-builders-status.ts +331 -0
  98. package/src/plugin/channel-runtime-builders.ts +25 -0
  99. package/src/plugin/channel-runtime-constants.ts +40 -0
  100. package/src/plugin/channel-runtime-types.ts +146 -0
  101. package/src/plugin/channel-send-runtime-group.ts +37 -0
  102. package/src/plugin/channel-send.ts +226 -0
  103. package/src/plugin/channel-utils.ts +102 -0
  104. package/src/plugin/config.ts +24 -3
  105. package/src/plugin/connection-handlers-helpers.ts +254 -0
  106. package/src/plugin/connection-handlers.ts +440 -0
  107. package/src/plugin/connection-state-helpers.ts +159 -0
  108. package/src/plugin/connection-state-runtime-group.ts +51 -0
  109. package/src/plugin/connection-state.ts +527 -0
  110. package/src/plugin/diagnostics-handlers.ts +211 -0
  111. package/src/plugin/error-message.ts +15 -0
  112. package/src/plugin/file-ack-runtime.ts +284 -0
  113. package/src/plugin/file-inbound-abort.ts +112 -0
  114. package/src/plugin/file-inbound-chunk.ts +146 -0
  115. package/src/plugin/file-inbound-complete.ts +153 -0
  116. package/src/plugin/file-inbound-handlers.ts +19 -0
  117. package/src/plugin/file-inbound-init.ts +122 -0
  118. package/src/plugin/file-inbound-runtime.ts +51 -0
  119. package/src/plugin/file-inbound-state.ts +62 -0
  120. package/src/plugin/file-transfer-logs.ts +227 -0
  121. package/src/plugin/file-transfer-orchestrator-chunk.ts +135 -0
  122. package/src/plugin/file-transfer-orchestrator.ts +304 -0
  123. package/src/plugin/file-transfer-runtime-group.ts +102 -0
  124. package/src/plugin/file-transfer-send.ts +89 -0
  125. package/src/plugin/file-transfer-setup.ts +206 -0
  126. package/src/plugin/gateway-event-context.ts +41 -0
  127. package/src/plugin/gateway-runtime.ts +17 -4
  128. package/src/plugin/inbound-acceptance.ts +107 -0
  129. package/src/plugin/inbound-handlers.ts +248 -0
  130. package/src/plugin/inbound-surface-handlers-group.ts +152 -0
  131. package/src/plugin/media-dedupe-runtime.ts +90 -0
  132. package/src/plugin/media-orchestrators-runtime-group.ts +316 -0
  133. package/src/plugin/message-ack-runtime.ts +284 -0
  134. package/src/plugin/message-send.ts +16 -6
  135. package/src/plugin/messaging.ts +98 -36
  136. package/src/plugin/outbound.ts +50 -8
  137. package/src/plugin/outbox-ack-logs.ts +136 -0
  138. package/src/plugin/outbox-ack-outcome.ts +128 -0
  139. package/src/plugin/outbox-drain-ack.ts +145 -0
  140. package/src/plugin/outbox-drain-failure.ts +84 -0
  141. package/src/plugin/outbox-drain-loop.ts +554 -0
  142. package/src/plugin/outbox-drain-post-push.ts +159 -0
  143. package/src/plugin/outbox-drain-runtime.ts +141 -0
  144. package/src/plugin/outbox-drain-schedule.ts +116 -0
  145. package/src/plugin/outbox-file-push-flow.ts +69 -0
  146. package/src/plugin/outbox-push-route-runtime-group.ts +81 -0
  147. package/src/plugin/outbox-push.ts +267 -0
  148. package/src/plugin/outbox-route.ts +181 -0
  149. package/src/plugin/outbox-text-push-flow.ts +90 -0
  150. package/src/plugin/runtime-diagnostics-assembler.ts +183 -0
  151. package/src/plugin/runtime-diagnostics-helpers.ts +302 -0
  152. package/src/plugin/runtime-diagnostics-payload-builders.ts +171 -0
  153. package/src/plugin/runtime-diagnostics-snapshot.ts +31 -0
  154. package/src/plugin/setup.ts +33 -6
  155. package/src/plugin/state-store.ts +249 -0
  156. package/src/plugin/state-transient-runtime-group.ts +105 -0
  157. package/src/plugin/status-runtime.ts +251 -0
  158. package/src/plugin/status.ts +33 -7
  159. package/src/plugin/target-runtime.ts +141 -0
  160. package/src/plugin/target-status-runtime-group.ts +130 -0
  161. package/src/plugin/transient-state-runtime.ts +82 -0
  162. package/src/runtime/outbound-ack-timeout.ts +5 -3
  163. package/src/runtime/outbound-flags.ts +24 -8
  164. package/src/runtime/status-snapshots.ts +36 -7
  165. package/src/runtime/status-worker.ts +34 -4
@@ -0,0 +1,249 @@
1
+ import {
2
+ dumpRegisterDriftSnapshot,
3
+ normalizeRegisterDriftSnapshot,
4
+ } from '../core/register-trace.ts';
5
+ import type { BncrRoute, OutboxEntry } from '../core/types.ts';
6
+ import {
7
+ readOpenClawJsonFileWithFallback,
8
+ writeOpenClawJsonFileAtomically,
9
+ } from '../openclaw/sdk-helpers.ts';
10
+ import type {
11
+ BncrPersistedAccountTimestamp,
12
+ BncrPersistedLastSession,
13
+ BncrPersistedSessionRoute,
14
+ PersistedState as BncrPersistedState,
15
+ } from './channel-runtime-types.ts';
16
+
17
+ type BncrPersistedStateStoreInput = {
18
+ outbox?: unknown;
19
+ deadLetter?: unknown;
20
+ sessionRoutes?: unknown;
21
+ lastSessionByAccount?: unknown;
22
+ lastActivityByAccount?: unknown;
23
+ lastInboundByAccount?: unknown;
24
+ lastOutboundByAccount?: unknown;
25
+ lastDriftSnapshot?: unknown;
26
+ };
27
+
28
+ type PersistedAccountTimestampInput = Partial<BncrPersistedAccountTimestamp>;
29
+ type PersistedLastSessionInput = Partial<BncrPersistedLastSession>;
30
+ type PersistedSessionRouteInput = Partial<BncrPersistedSessionRoute>;
31
+
32
+ export function createBncrStateStore(runtime: {
33
+ getStatePath: () => string | null;
34
+ now: () => number;
35
+ asString: (value: unknown, fallback?: string) => string;
36
+ finiteNumberOr: (value: unknown, fallback: number) => number;
37
+ normalizeAccountId: (accountId: string) => string;
38
+ normalizeStoredSessionKey: (
39
+ sessionKey: string,
40
+ canonicalAgentId?: string,
41
+ ) => {
42
+ sessionKey: string;
43
+ route: BncrRoute;
44
+ } | null;
45
+ parseRouteLike: (value: unknown) => BncrRoute | null;
46
+ routeKey: (accountId: string, route: BncrRoute) => string;
47
+ formatDisplayScope: (route: BncrRoute) => string;
48
+ canonicalAgentId: () => string;
49
+ normalizePersistedOutboxEntry: (entry: unknown) => OutboxEntry | null;
50
+ maxDeadLetterEntries: number;
51
+ maxSessionRouteEntries: number;
52
+ maxAccountActivityEntries: number;
53
+ outbox: Map<string, OutboxEntry>;
54
+ getDeadLetter: () => OutboxEntry[];
55
+ setDeadLetter: (entries: OutboxEntry[]) => void;
56
+ sessionRoutes: Map<string, { accountId: string; route: BncrRoute; updatedAt: number }>;
57
+ routeAliases: Map<string, { accountId: string; route: BncrRoute; updatedAt: number }>;
58
+ lastSessionByAccount: Map<string, { sessionKey: string; scope: string; updatedAt: number }>;
59
+ lastActivityByAccount: Map<string, number>;
60
+ lastInboundByAccount: Map<string, number>;
61
+ lastOutboundByAccount: Map<string, number>;
62
+ getLastDriftSnapshot: () => BncrPersistedState['lastDriftSnapshot'];
63
+ setLastDriftSnapshot: (value: BncrPersistedState['lastDriftSnapshot']) => void;
64
+ }) {
65
+ function loadPersistedAccountTimestampMap(target: Map<string, number>, persisted: unknown): void {
66
+ target.clear();
67
+ const items = Array.isArray(persisted)
68
+ ? (persisted.slice(-runtime.maxAccountActivityEntries) as PersistedAccountTimestampInput[])
69
+ : [];
70
+ for (const item of items) {
71
+ if (!item || typeof item !== 'object') continue;
72
+ const accountId = runtime.normalizeAccountId(runtime.asString(item.accountId || ''));
73
+ const updatedAt = runtime.finiteNumberOr(item.updatedAt, 0);
74
+ if (updatedAt <= 0) continue;
75
+ target.set(accountId, updatedAt);
76
+ }
77
+ }
78
+
79
+ function dumpPersistedAccountTimestampMap(source: Map<string, number>) {
80
+ return Array.from(source.entries())
81
+ .map(([accountId, updatedAt]) => ({
82
+ accountId,
83
+ updatedAt,
84
+ }))
85
+ .slice(-runtime.maxAccountActivityEntries);
86
+ }
87
+
88
+ function loadPersistedLastSessionMap(persisted: unknown): void {
89
+ runtime.lastSessionByAccount.clear();
90
+ const items = Array.isArray(persisted)
91
+ ? (persisted.slice(-runtime.maxAccountActivityEntries) as PersistedLastSessionInput[])
92
+ : [];
93
+ for (const item of items) {
94
+ if (!item || typeof item !== 'object') continue;
95
+ const accountId = runtime.normalizeAccountId(runtime.asString(item.accountId || ''));
96
+ const normalized = runtime.normalizeStoredSessionKey(
97
+ runtime.asString(item.sessionKey || ''),
98
+ runtime.canonicalAgentId(),
99
+ );
100
+ const updatedAt = runtime.finiteNumberOr(item.updatedAt, 0);
101
+ if (!normalized || updatedAt <= 0) continue;
102
+
103
+ runtime.lastSessionByAccount.set(accountId, {
104
+ sessionKey: normalized.sessionKey,
105
+ scope: runtime.formatDisplayScope(normalized.route),
106
+ updatedAt,
107
+ });
108
+ }
109
+ }
110
+
111
+ function dumpPersistedLastSessionMap() {
112
+ return Array.from(runtime.lastSessionByAccount.entries())
113
+ .map(([accountId, v]) => ({
114
+ accountId,
115
+ sessionKey: v.sessionKey,
116
+ scope: v.scope,
117
+ updatedAt: v.updatedAt,
118
+ }))
119
+ .slice(-runtime.maxAccountActivityEntries);
120
+ }
121
+
122
+ function loadPersistedSessionRoutes(persisted: unknown): void {
123
+ runtime.sessionRoutes.clear();
124
+ runtime.routeAliases.clear();
125
+ const items = Array.isArray(persisted)
126
+ ? (persisted.slice(-runtime.maxSessionRouteEntries) as PersistedSessionRouteInput[])
127
+ : [];
128
+ for (const item of items) {
129
+ if (!item || typeof item !== 'object') continue;
130
+ const normalized = runtime.normalizeStoredSessionKey(
131
+ runtime.asString(item.sessionKey || ''),
132
+ runtime.canonicalAgentId(),
133
+ );
134
+ if (!normalized) continue;
135
+
136
+ const route = runtime.parseRouteLike(item.route) || normalized.route;
137
+ const accountId = runtime.normalizeAccountId(runtime.asString(item.accountId || ''));
138
+ const updatedAt = runtime.finiteNumberOr(item.updatedAt, runtime.now());
139
+ const info = { accountId, route, updatedAt };
140
+
141
+ runtime.sessionRoutes.set(normalized.sessionKey, info);
142
+ runtime.routeAliases.set(runtime.routeKey(accountId, route), info);
143
+ }
144
+ }
145
+
146
+ function dumpPersistedSessionRoutes() {
147
+ return Array.from(runtime.sessionRoutes.entries())
148
+ .map(([sessionKey, v]) => ({
149
+ sessionKey,
150
+ accountId: v.accountId,
151
+ route: v.route,
152
+ updatedAt: v.updatedAt,
153
+ }))
154
+ .slice(-runtime.maxSessionRouteEntries);
155
+ }
156
+
157
+ function backfillAccountActivityFromSessionRoutes(): void {
158
+ if (runtime.lastSessionByAccount.size > 0 || runtime.sessionRoutes.size === 0) return;
159
+
160
+ for (const [sessionKey, info] of runtime.sessionRoutes.entries()) {
161
+ const acc = runtime.normalizeAccountId(info.accountId);
162
+ const updatedAt = runtime.finiteNumberOr(info.updatedAt, 0);
163
+ if (updatedAt <= 0) continue;
164
+
165
+ const current = runtime.lastSessionByAccount.get(acc);
166
+ if (!current || updatedAt >= current.updatedAt) {
167
+ runtime.lastSessionByAccount.set(acc, {
168
+ sessionKey,
169
+ scope: runtime.formatDisplayScope(info.route),
170
+ updatedAt,
171
+ });
172
+ }
173
+
174
+ const lastAct = runtime.lastActivityByAccount.get(acc) || 0;
175
+ if (updatedAt > lastAct) runtime.lastActivityByAccount.set(acc, updatedAt);
176
+
177
+ const lastIn = runtime.lastInboundByAccount.get(acc) || 0;
178
+ if (updatedAt > lastIn) runtime.lastInboundByAccount.set(acc, updatedAt);
179
+ }
180
+ }
181
+
182
+ async function loadState() {
183
+ const statePath = runtime.getStatePath();
184
+ if (!statePath) return;
185
+ const loaded = await readOpenClawJsonFileWithFallback(statePath, {
186
+ outbox: [],
187
+ deadLetter: [],
188
+ sessionRoutes: [],
189
+ });
190
+ const data = loaded.value as BncrPersistedStateStoreInput;
191
+
192
+ runtime.outbox.clear();
193
+ for (const entry of Array.isArray(data.outbox) ? data.outbox : []) {
194
+ const migratedEntry = runtime.normalizePersistedOutboxEntry(entry);
195
+ if (!migratedEntry) continue;
196
+ runtime.outbox.set(migratedEntry.messageId, migratedEntry);
197
+ }
198
+
199
+ const deadLetter: OutboxEntry[] = [];
200
+ const persistedDeadLetter = Array.isArray(data.deadLetter)
201
+ ? data.deadLetter.slice(-runtime.maxDeadLetterEntries)
202
+ : [];
203
+ for (const entry of persistedDeadLetter) {
204
+ const migratedEntry = runtime.normalizePersistedOutboxEntry(entry);
205
+ if (!migratedEntry) continue;
206
+ deadLetter.push(migratedEntry);
207
+ }
208
+ runtime.setDeadLetter(deadLetter);
209
+
210
+ loadPersistedSessionRoutes(data.sessionRoutes);
211
+ loadPersistedLastSessionMap(data.lastSessionByAccount);
212
+ loadPersistedAccountTimestampMap(runtime.lastActivityByAccount, data.lastActivityByAccount);
213
+ loadPersistedAccountTimestampMap(runtime.lastInboundByAccount, data.lastInboundByAccount);
214
+ loadPersistedAccountTimestampMap(runtime.lastOutboundByAccount, data.lastOutboundByAccount);
215
+
216
+ runtime.setLastDriftSnapshot(normalizeRegisterDriftSnapshot(data.lastDriftSnapshot));
217
+ backfillAccountActivityFromSessionRoutes();
218
+ }
219
+
220
+ async function flushState() {
221
+ const statePath = runtime.getStatePath();
222
+ if (!statePath) return;
223
+
224
+ const data: BncrPersistedState = {
225
+ outbox: Array.from(runtime.outbox.values()),
226
+ deadLetter: runtime.getDeadLetter().slice(-runtime.maxDeadLetterEntries),
227
+ sessionRoutes: dumpPersistedSessionRoutes(),
228
+ lastSessionByAccount: dumpPersistedLastSessionMap(),
229
+ lastActivityByAccount: dumpPersistedAccountTimestampMap(runtime.lastActivityByAccount),
230
+ lastInboundByAccount: dumpPersistedAccountTimestampMap(runtime.lastInboundByAccount),
231
+ lastOutboundByAccount: dumpPersistedAccountTimestampMap(runtime.lastOutboundByAccount),
232
+ lastDriftSnapshot: dumpRegisterDriftSnapshot(runtime.getLastDriftSnapshot() ?? null),
233
+ };
234
+
235
+ await writeOpenClawJsonFileAtomically(statePath, data);
236
+ }
237
+
238
+ return {
239
+ loadPersistedAccountTimestampMap,
240
+ dumpPersistedAccountTimestampMap,
241
+ loadPersistedLastSessionMap,
242
+ dumpPersistedLastSessionMap,
243
+ loadPersistedSessionRoutes,
244
+ dumpPersistedSessionRoutes,
245
+ backfillAccountActivityFromSessionRoutes,
246
+ loadState,
247
+ flushState,
248
+ };
249
+ }
@@ -0,0 +1,105 @@
1
+ import type { RegisterDriftSnapshot } from '../core/register-trace.ts';
2
+ import type {
3
+ BncrConnection,
4
+ BncrRoute,
5
+ FileRecvTransferState,
6
+ FileSendTransferState,
7
+ OutboxEntry,
8
+ } from '../core/types.ts';
9
+ import { createBncrStateStore } from './state-store.ts';
10
+ import { createBncrTransientStateRuntime } from './transient-state-runtime.ts';
11
+
12
+ type StoredRouteRecord = { accountId: string; route: BncrRoute; updatedAt: number };
13
+ type StoredLastSessionRecord = { sessionKey: string; scope: string; updatedAt: number };
14
+
15
+ export function createBncrStateTransientRuntimeGroup(runtime: {
16
+ bridgeId: string;
17
+ getStatePath: () => string | null;
18
+ now: () => number;
19
+ asString: (value: unknown, fallback?: string) => string;
20
+ finiteNumberOr: (value: unknown, fallback: number) => number;
21
+ normalizeAccountId: (accountId: string) => string;
22
+ normalizeStoredSessionKey: (
23
+ sessionKey: string,
24
+ canonicalAgentId?: string,
25
+ ) => { sessionKey: string; route: BncrRoute } | null;
26
+ parseRouteLike: (value: unknown) => BncrRoute | null;
27
+ routeKey: (accountId: string, route: BncrRoute) => string;
28
+ formatDisplayScope: (route: BncrRoute) => string;
29
+ canonicalAgentId: () => string;
30
+ normalizePersistedOutboxEntry: (entry: unknown) => OutboxEntry | null;
31
+ maxDeadLetterEntries: number;
32
+ maxSessionRouteEntries: number;
33
+ maxAccountActivityEntries: number;
34
+ outbox: Map<string, OutboxEntry>;
35
+ getDeadLetter: () => OutboxEntry[];
36
+ setDeadLetter: (entries: OutboxEntry[]) => void;
37
+ sessionRoutes: Map<string, StoredRouteRecord>;
38
+ routeAliases: Map<string, StoredRouteRecord>;
39
+ lastSessionByAccount: Map<string, StoredLastSessionRecord>;
40
+ lastActivityByAccount: Map<string, number>;
41
+ lastInboundByAccount: Map<string, number>;
42
+ lastOutboundByAccount: Map<string, number>;
43
+ getLastDriftSnapshot: () => RegisterDriftSnapshot | null | undefined;
44
+ setLastDriftSnapshot: (value: RegisterDriftSnapshot | null | undefined) => void;
45
+ connectTtlMs: number;
46
+ fileTransferKeepMs: number;
47
+ fileTransferTerminalKeepMs: number;
48
+ fileTransferAckTtlMs: number;
49
+ connections: Map<string, BncrConnection>;
50
+ activeConnectionByAccount: Map<string, string>;
51
+ recentInbound: Map<string, number>;
52
+ fileSendTransfers: Map<string, FileSendTransferState>;
53
+ fileRecvTransfers: Map<string, FileRecvTransferState>;
54
+ earlyFileAcks: Map<string, { at: number }>;
55
+ logInfo: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) => void;
56
+ }) {
57
+ const stateStore = createBncrStateStore({
58
+ getStatePath: runtime.getStatePath,
59
+ now: runtime.now,
60
+ asString: runtime.asString,
61
+ finiteNumberOr: runtime.finiteNumberOr,
62
+ normalizeAccountId: runtime.normalizeAccountId,
63
+ normalizeStoredSessionKey: runtime.normalizeStoredSessionKey,
64
+ parseRouteLike: runtime.parseRouteLike,
65
+ routeKey: runtime.routeKey,
66
+ formatDisplayScope: runtime.formatDisplayScope,
67
+ canonicalAgentId: runtime.canonicalAgentId,
68
+ normalizePersistedOutboxEntry: runtime.normalizePersistedOutboxEntry,
69
+ maxDeadLetterEntries: runtime.maxDeadLetterEntries,
70
+ maxSessionRouteEntries: runtime.maxSessionRouteEntries,
71
+ maxAccountActivityEntries: runtime.maxAccountActivityEntries,
72
+ outbox: runtime.outbox,
73
+ getDeadLetter: runtime.getDeadLetter,
74
+ setDeadLetter: runtime.setDeadLetter,
75
+ sessionRoutes: runtime.sessionRoutes,
76
+ routeAliases: runtime.routeAliases,
77
+ lastSessionByAccount: runtime.lastSessionByAccount,
78
+ lastActivityByAccount: runtime.lastActivityByAccount,
79
+ lastInboundByAccount: runtime.lastInboundByAccount,
80
+ lastOutboundByAccount: runtime.lastOutboundByAccount,
81
+ getLastDriftSnapshot: runtime.getLastDriftSnapshot,
82
+ setLastDriftSnapshot: runtime.setLastDriftSnapshot,
83
+ });
84
+
85
+ const transientStateRuntime = createBncrTransientStateRuntime({
86
+ now: runtime.now,
87
+ connectTtlMs: runtime.connectTtlMs,
88
+ fileTransferKeepMs: runtime.fileTransferKeepMs,
89
+ fileTransferTerminalKeepMs: runtime.fileTransferTerminalKeepMs,
90
+ fileTransferAckTtlMs: runtime.fileTransferAckTtlMs,
91
+ connections: runtime.connections,
92
+ activeConnectionByAccount: runtime.activeConnectionByAccount,
93
+ recentInbound: runtime.recentInbound,
94
+ fileSendTransfers: runtime.fileSendTransfers,
95
+ fileRecvTransfers: runtime.fileRecvTransfers,
96
+ earlyFileAcks: runtime.earlyFileAcks,
97
+ logInfo: runtime.logInfo,
98
+ bridgeId: runtime.bridgeId,
99
+ });
100
+
101
+ return {
102
+ stateStore,
103
+ transientStateRuntime,
104
+ };
105
+ }
@@ -0,0 +1,251 @@
1
+ import path from 'node:path';
2
+ import { normalizeAccountId } from '../core/accounts.ts';
3
+ import { buildDownlinkHealth as buildDownlinkHealthFromRuntime } from '../core/downlink-health.ts';
4
+ import {
5
+ buildAccountRuntimeSnapshot,
6
+ buildIntegratedDiagnostics as buildIntegratedDiagnosticsFromRuntime,
7
+ buildStatusHeadlineFromRuntime,
8
+ buildStatusMetaFromRuntime,
9
+ } from '../core/status.ts';
10
+ import type {
11
+ BncrAckObservability,
12
+ BncrAckStrategy,
13
+ BncrRuntimeLastSession,
14
+ OutboxEntry,
15
+ } from '../core/types.ts';
16
+ import { buildBncrRuntimeFlags, buildBncrRuntimeStatusInput } from '../runtime/outbound-flags.ts';
17
+ import type { RuntimeStatusSnapshots } from '../runtime/status-snapshots.ts';
18
+ import { buildRuntimeStatusSnapshots } from '../runtime/status-snapshots.ts';
19
+
20
+ type RuntimeStatusInput = Parameters<typeof buildIntegratedDiagnosticsFromRuntime>[0];
21
+
22
+ export type BncrStatusRuntimeHelpers = {
23
+ api: unknown;
24
+ getPluginRoot: () => string | null;
25
+ startedAt: number;
26
+ debugVerbose: boolean;
27
+ adaptiveAckTimeoutEnabled: boolean;
28
+ defaultMessageAckTimeoutMs: number;
29
+ fileAckTimeoutMs: number;
30
+ maxAckTimeoutMs: number;
31
+ now: () => number;
32
+ resolveMessageAckTimeoutMs: (accountId?: string) => number;
33
+ isOnline: (accountId: string) => boolean;
34
+ outboxValues: () => Iterable<OutboxEntry>;
35
+ deadLetterEntries: () => OutboxEntry[];
36
+ sessionRouteValues: () => Iterable<{ accountId: string }>;
37
+ countInvalidOutboxSessionKeys: (accountId: string) => number;
38
+ countLegacyAccountResidue: (accountId: string) => number;
39
+ connectEventsByAccount: Map<string, number>;
40
+ inboundEventsByAccount: Map<string, number>;
41
+ activityEventsByAccount: Map<string, number>;
42
+ ackEventsByAccount: Map<string, number>;
43
+ activeConnectionCount: (accountId: string) => number;
44
+ lastSessionByAccount: Map<string, BncrRuntimeLastSession>;
45
+ lastActivityByAccount: Map<string, number>;
46
+ lastInboundByAccount: Map<string, number>;
47
+ lastOutboundByAccount: Map<string, number>;
48
+ buildRuntimeAckObservability: (accountId: string) => BncrAckObservability;
49
+ buildRuntimeAckStrategy: (ackObservability: BncrAckObservability) => BncrAckStrategy;
50
+ lastAckOkByAccount: Map<string, number>;
51
+ lastAckTimeoutByAccount: Map<string, number>;
52
+ getAckTimeoutCount: (accountId: string) => number;
53
+ getAccountPendingOutboxEntries: (accountId: string) => OutboxEntry[];
54
+ getAccountDeadLetterEntries: (accountId: string) => OutboxEntry[];
55
+ connectionsValues: () => Iterable<{ lastSeenAt: number }>;
56
+ connectTtlMs: number;
57
+ };
58
+
59
+ export function createBncrStatusRuntime(runtime: BncrStatusRuntimeHelpers) {
60
+ const buildRuntimeFlags = (accountId?: string) =>
61
+ buildBncrRuntimeFlags({
62
+ api: runtime.api,
63
+ accountId,
64
+ resolveMessageAckTimeoutMs: (acc?: string) => runtime.resolveMessageAckTimeoutMs(acc),
65
+ adaptiveAckTimeoutEnabled: runtime.adaptiveAckTimeoutEnabled,
66
+ defaultMessageAckTimeoutMs: runtime.defaultMessageAckTimeoutMs,
67
+ fileAckTimeoutMs: runtime.fileAckTimeoutMs,
68
+ debugVerbose: runtime.debugVerbose,
69
+ });
70
+
71
+ const buildAccountQueueCounters = (accountId: string) => ({
72
+ activeConnections: runtime.activeConnectionCount(accountId),
73
+ pending: runtime.getAccountPendingOutboxEntries(accountId).length,
74
+ deadLetter: runtime.getAccountDeadLetterEntries(accountId).length,
75
+ });
76
+
77
+ const buildRuntimeStatusCore = (accountId: string, snapshots: RuntimeStatusSnapshots) => {
78
+ const acc = normalizeAccountId(accountId);
79
+ const counters = buildAccountQueueCounters(acc);
80
+ return {
81
+ accountId: acc,
82
+ connected: runtime.isOnline(acc),
83
+ pending: counters.pending,
84
+ deadLetter: counters.deadLetter,
85
+ activeConnections: counters.activeConnections,
86
+ connectEvents: snapshots.eventCounters?.connectEvents ?? 0,
87
+ inboundEvents: snapshots.eventCounters?.inboundEvents ?? 0,
88
+ activityEvents: snapshots.eventCounters?.activityEvents ?? 0,
89
+ ackEvents: snapshots.eventCounters?.ackEvents ?? 0,
90
+ lastSession: snapshots.activitySnapshot?.lastSession ?? null,
91
+ lastActivityAt: snapshots.activitySnapshot?.lastActivityAt ?? null,
92
+ lastInboundAt: snapshots.activitySnapshot?.lastInboundAt ?? null,
93
+ lastOutboundAt: snapshots.activitySnapshot?.lastOutboundAt ?? null,
94
+ sessionRoutesCount: snapshots.queueSnapshot?.sessionRoutesCount ?? 0,
95
+ invalidOutboxSessionKeys: snapshots.queueSnapshot?.invalidOutboxSessionKeys ?? 0,
96
+ legacyAccountResidue: snapshots.queueSnapshot?.legacyAccountResidue ?? 0,
97
+ };
98
+ };
99
+
100
+ const buildRuntimeStatusInput = (
101
+ accountId: string,
102
+ overrides: {
103
+ running?: boolean;
104
+ invalidOutboxSessionKeys?: number;
105
+ legacyAccountResidue?: number;
106
+ } = {},
107
+ ): RuntimeStatusInput => {
108
+ const acc = normalizeAccountId(accountId);
109
+ const snapshots: RuntimeStatusSnapshots = buildRuntimeStatusSnapshots({
110
+ accountId: acc,
111
+ outboxEntries: runtime.outboxValues(),
112
+ deadLetterEntries: runtime.deadLetterEntries(),
113
+ sessionRouteEntries: runtime.sessionRouteValues(),
114
+ countInvalidOutboxSessionKeys: (snapshotAccountId) =>
115
+ normalizeAccountId(snapshotAccountId) === acc &&
116
+ typeof overrides.invalidOutboxSessionKeys === 'number'
117
+ ? overrides.invalidOutboxSessionKeys
118
+ : runtime.countInvalidOutboxSessionKeys(snapshotAccountId),
119
+ countLegacyAccountResidue: (snapshotAccountId) =>
120
+ normalizeAccountId(snapshotAccountId) === acc &&
121
+ typeof overrides.legacyAccountResidue === 'number'
122
+ ? overrides.legacyAccountResidue
123
+ : runtime.countLegacyAccountResidue(snapshotAccountId),
124
+ connectEventsByAccount: runtime.connectEventsByAccount,
125
+ inboundEventsByAccount: runtime.inboundEventsByAccount,
126
+ activityEventsByAccount: runtime.activityEventsByAccount,
127
+ ackEventsByAccount: runtime.ackEventsByAccount,
128
+ activeConnectionCount: (snapshotAccountId) =>
129
+ runtime.activeConnectionCount(snapshotAccountId),
130
+ lastSessionByAccount: runtime.lastSessionByAccount,
131
+ lastActivityByAccount: runtime.lastActivityByAccount,
132
+ lastInboundByAccount: runtime.lastInboundByAccount,
133
+ lastOutboundByAccount: runtime.lastOutboundByAccount,
134
+ });
135
+ const core = buildRuntimeStatusCore(acc, snapshots);
136
+ const base = buildBncrRuntimeStatusInput({
137
+ accountId: core.accountId,
138
+ connected: core.connected,
139
+ ...snapshots,
140
+ startedAt: runtime.startedAt || runtime.now(),
141
+ running: overrides.running,
142
+ channelRoot: runtime.getPluginRoot() || path.join(process.cwd(), 'plugins', 'bncr'),
143
+ });
144
+ return {
145
+ ...core,
146
+ startedAt: Number(base.startedAt || runtime.startedAt || runtime.now()),
147
+ running: base.running,
148
+ channelRoot: base.channelRoot,
149
+ };
150
+ };
151
+
152
+ const buildIntegratedDiagnostics = (
153
+ accountId: string,
154
+ runtimeStatusInput?: RuntimeStatusInput,
155
+ ) => {
156
+ const ackObservability = runtime.buildRuntimeAckObservability(accountId);
157
+ const ackStrategy = runtime.buildRuntimeAckStrategy(ackObservability);
158
+ return {
159
+ ...buildIntegratedDiagnosticsFromRuntime(
160
+ runtimeStatusInput || buildRuntimeStatusInput(accountId),
161
+ ),
162
+ ackObservability,
163
+ ackStrategy,
164
+ };
165
+ };
166
+
167
+ const buildDownlinkHealth = (accountId: string) => {
168
+ const acc = normalizeAccountId(accountId);
169
+ return buildDownlinkHealthFromRuntime({
170
+ accountId: acc,
171
+ now: runtime.now(),
172
+ outboxEntries: runtime.outboxValues(),
173
+ lastAckOkAt: runtime.lastAckOkByAccount.get(acc) || null,
174
+ lastAckTimeoutAt: runtime.lastAckTimeoutByAccount.get(acc) || null,
175
+ recentAckTimeoutCount: runtime.getAckTimeoutCount(acc),
176
+ activeConnectionCount: runtime.activeConnectionCount(acc),
177
+ lastInboundAt: runtime.lastInboundByAccount.get(acc) || null,
178
+ lastActivityAt: runtime.lastActivityByAccount.get(acc) || null,
179
+ onlineByConn: runtime.isOnline(acc),
180
+ });
181
+ };
182
+
183
+ const buildStatusMeta = (accountId: string) =>
184
+ buildStatusMetaFromRuntime(buildRuntimeStatusInput(accountId));
185
+
186
+ const getAccountRuntimeSnapshot = (
187
+ accountId: string,
188
+ runtimeStatusInput = buildRuntimeStatusInput(accountId, { running: true }),
189
+ ) => {
190
+ const snapshot = buildAccountRuntimeSnapshot(runtimeStatusInput);
191
+ const ackObservability = runtime.buildRuntimeAckObservability(accountId);
192
+ const ackStrategy = runtime.buildRuntimeAckStrategy(ackObservability);
193
+ return {
194
+ ...snapshot,
195
+ ackObservability,
196
+ ackStrategy,
197
+ diagnostics: {
198
+ ...(snapshot.diagnostics || {}),
199
+ ackObservability,
200
+ ackStrategy,
201
+ },
202
+ meta: {
203
+ ...(snapshot.meta || {}),
204
+ ackObservability,
205
+ ackStrategy,
206
+ diagnostics: {
207
+ ...(snapshot.meta?.diagnostics || {}),
208
+ ackObservability,
209
+ ackStrategy,
210
+ },
211
+ },
212
+ };
213
+ };
214
+
215
+ const buildStatusHeadline = (accountId: string) =>
216
+ buildStatusHeadlineFromRuntime(buildRuntimeStatusInput(accountId));
217
+
218
+ const getStatusHeadline = (accountId: string) => buildStatusHeadline(accountId);
219
+
220
+ const getChannelSummary = (defaultAccountId: string) => {
221
+ const accountId = normalizeAccountId(defaultAccountId);
222
+ const runtimeSnapshot = getAccountRuntimeSnapshot(accountId);
223
+ const headline = buildStatusHeadline(accountId);
224
+
225
+ if (runtimeSnapshot.connected) {
226
+ return { linked: true, self: { e164: headline } };
227
+ }
228
+
229
+ const currentTime = runtime.now();
230
+ for (const connection of runtime.connectionsValues()) {
231
+ if (currentTime - connection.lastSeenAt <= runtime.connectTtlMs) {
232
+ return { linked: true, self: { e164: headline } };
233
+ }
234
+ }
235
+
236
+ return { linked: false, self: { e164: headline } };
237
+ };
238
+
239
+ return {
240
+ buildRuntimeFlags,
241
+ buildAccountQueueCounters,
242
+ buildRuntimeStatusInput,
243
+ buildIntegratedDiagnostics,
244
+ buildDownlinkHealth,
245
+ buildStatusMeta,
246
+ getAccountRuntimeSnapshot,
247
+ buildStatusHeadline,
248
+ getStatusHeadline,
249
+ getChannelSummary,
250
+ };
251
+ }
@@ -5,11 +5,31 @@ import {
5
5
  } from '../core/accounts.ts';
6
6
  import { buildAccountStatusSnapshot } from '../core/status.ts';
7
7
  import { createOpenClawDefaultChannelRuntimeState } from '../openclaw/sdk-helpers.ts';
8
+ import type { BncrChannelConfigRoot, BncrStatusRuntimeSnapshot } from './channel-runtime-types.ts';
9
+
10
+ type StatusSurfaceSummaryArgs = { defaultAccountId?: string };
11
+
12
+ type StatusSurfaceAccount = { accountId?: string; name?: string; enabled?: boolean };
13
+
14
+ type StatusSurfaceAccountSnapshotArgs = {
15
+ account?: StatusSurfaceAccount;
16
+ runtime?: BncrStatusRuntimeSnapshot;
17
+ };
18
+
19
+ type StatusSurfaceAccountStateArgs = {
20
+ enabled?: boolean;
21
+ configured?: boolean;
22
+ account?: StatusSurfaceAccount;
23
+ cfg: BncrChannelConfigRoot;
24
+ runtime?: BncrStatusRuntimeSnapshot;
25
+ };
8
26
 
9
27
  export type BncrStatusBridge = {
10
- getChannelSummary: (defaultAccountId: string) => unknown | Promise<unknown>;
11
- getAccountRuntimeSnapshot: (accountId?: string) => unknown;
12
- getStatusHeadline: (accountId?: string) => unknown;
28
+ getChannelSummary: (
29
+ defaultAccountId: string,
30
+ ) => Record<string, unknown> | Promise<Record<string, unknown>>;
31
+ getAccountRuntimeSnapshot: (accountId?: string) => BncrStatusRuntimeSnapshot;
32
+ getStatusHeadline: (accountId?: string) => string;
13
33
  };
14
34
 
15
35
  export function createBncrStatusSurface(getBridge: () => BncrStatusBridge) {
@@ -17,10 +37,10 @@ export function createBncrStatusSurface(getBridge: () => BncrStatusBridge) {
17
37
  defaultRuntime: createOpenClawDefaultChannelRuntimeState(BNCR_DEFAULT_ACCOUNT_ID, {
18
38
  mode: 'ws-offline',
19
39
  }),
20
- buildChannelSummary: async ({ defaultAccountId }: any) => {
40
+ buildChannelSummary: async ({ defaultAccountId }: StatusSurfaceSummaryArgs) => {
21
41
  return getBridge().getChannelSummary(defaultAccountId || BNCR_DEFAULT_ACCOUNT_ID);
22
42
  },
23
- buildAccountSnapshot: async ({ account, runtime }: any) => {
43
+ buildAccountSnapshot: async ({ account, runtime }: StatusSurfaceAccountSnapshotArgs) => {
24
44
  const runtimeBridge = getBridge();
25
45
  const accountId = account?.accountId || BNCR_DEFAULT_ACCOUNT_ID;
26
46
  const snapshotAccount = {
@@ -37,12 +57,18 @@ export function createBncrStatusSurface(getBridge: () => BncrStatusBridge) {
37
57
  displayName: resolveDefaultDisplayName(account?.name, accountId),
38
58
  });
39
59
  },
40
- resolveAccountState: ({ enabled, configured, account, cfg, runtime }: any) => {
60
+ resolveAccountState: ({
61
+ enabled,
62
+ configured,
63
+ account,
64
+ cfg,
65
+ runtime,
66
+ }: StatusSurfaceAccountStateArgs) => {
41
67
  if (!enabled) return 'disabled';
42
68
  const resolved = resolveAccount(cfg, account?.accountId);
43
69
  if (!(resolved.enabled && configured)) return 'not configured';
44
70
  const rt = runtime || getBridge().getAccountRuntimeSnapshot(account?.accountId);
45
- return (rt as any)?.connected ? 'linked' : 'configured';
71
+ return rt?.connected ? 'linked' : 'configured';
46
72
  },
47
73
  };
48
74
  }