@xmoxmo/bncr 0.2.4 → 0.2.6

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 +68 -5
  2. package/package.json +1 -1
  3. package/src/channel.ts +2985 -1706
  4. package/src/core/connection-capability.ts +70 -0
  5. package/src/core/connection-reachability.ts +168 -0
  6. package/src/core/diagnostics.ts +54 -0
  7. package/src/core/downlink-health.ts +61 -0
  8. package/src/core/extended-diagnostics.ts +65 -0
  9. package/src/core/lease-state.ts +94 -0
  10. package/src/core/outbox-enqueue.ts +22 -0
  11. package/src/core/outbox-entry-builders.ts +92 -0
  12. package/src/core/outbox-file-transfer-bookkeeping.ts +31 -0
  13. package/src/core/outbox-file-transfer-failure.ts +25 -0
  14. package/src/core/outbox-file-transfer-guards.ts +66 -0
  15. package/src/core/outbox-file-transfer-prep.ts +31 -0
  16. package/src/core/outbox-file-transfer-success.ts +34 -0
  17. package/src/core/outbox-push-args.ts +67 -0
  18. package/src/core/outbox-queue.ts +69 -0
  19. package/src/core/outbox-summary.ts +14 -0
  20. package/src/core/outbox-text-push-failure.ts +10 -0
  21. package/src/core/outbox-text-push-guards.ts +51 -0
  22. package/src/core/outbox-text-push-prep.ts +36 -0
  23. package/src/core/outbox-text-push-success.ts +62 -0
  24. package/src/core/policy.ts +9 -0
  25. package/src/core/register-trace.ts +115 -0
  26. package/src/core/status.ts +57 -0
  27. package/src/core/types.ts +1 -0
  28. package/src/messaging/inbound/commands.ts +318 -75
  29. package/src/messaging/inbound/dispatch.ts +435 -139
  30. package/src/messaging/inbound/parse.ts +8 -0
  31. package/src/messaging/inbound/session-label.ts +115 -0
  32. package/src/messaging/outbound/diagnostics.ts +262 -0
  33. package/src/messaging/outbound/media-dedupe.ts +51 -0
  34. package/src/messaging/outbound/media.ts +3 -1
  35. package/src/messaging/outbound/queue-selectors.ts +191 -0
  36. package/src/messaging/outbound/reasons.ts +52 -0
  37. package/src/messaging/outbound/reply-enqueue.ts +329 -0
  38. package/src/messaging/outbound/reply-target-policy.ts +13 -0
  39. package/src/messaging/outbound/retry-policy.ts +142 -0
  40. package/src/messaging/outbound/send.ts +6 -0
  41. package/src/messaging/outbound/session-route.ts +34 -5
@@ -0,0 +1,66 @@
1
+ import type { BncrConnection, OutboxEntry } from './types.ts';
2
+
3
+ export type FileTransferGuardResult =
4
+ | { ok: false; reason: 'no-gateway-context'; lastError: 'gateway context unavailable' }
5
+ | {
6
+ ok: false;
7
+ reason: 'no-active-connection';
8
+ lastError: 'no active bncr client for file chunk transfer';
9
+ recentInboundReachable: boolean;
10
+ }
11
+ | { ok: false; reason: 'media-url-missing'; lastError: 'file transfer mediaUrl missing' }
12
+ | {
13
+ ok: true;
14
+ owner: BncrConnection | null;
15
+ connIds: Set<string>;
16
+ recentInboundReachable: boolean;
17
+ routeReason: string;
18
+ mediaUrl: string;
19
+ };
20
+
21
+ export function resolveFileTransferGuard(args: {
22
+ gatewayContext: unknown;
23
+ entry: OutboxEntry;
24
+ owner: BncrConnection | null;
25
+ routeSelection: {
26
+ connIds: Iterable<string>;
27
+ recentInboundReachable: boolean;
28
+ routeReason: string;
29
+ };
30
+ mediaUrl: string;
31
+ }): FileTransferGuardResult {
32
+ if (!args.gatewayContext) {
33
+ return {
34
+ ok: false,
35
+ reason: 'no-gateway-context',
36
+ lastError: 'gateway context unavailable',
37
+ };
38
+ }
39
+
40
+ const connIds = new Set(args.routeSelection.connIds);
41
+ if (!connIds.size) {
42
+ return {
43
+ ok: false,
44
+ reason: 'no-active-connection',
45
+ lastError: 'no active bncr client for file chunk transfer',
46
+ recentInboundReachable: args.routeSelection.recentInboundReachable,
47
+ };
48
+ }
49
+
50
+ if (!args.mediaUrl) {
51
+ return {
52
+ ok: false,
53
+ reason: 'media-url-missing',
54
+ lastError: 'file transfer mediaUrl missing',
55
+ };
56
+ }
57
+
58
+ return {
59
+ ok: true,
60
+ owner: args.owner,
61
+ connIds,
62
+ recentInboundReachable: args.routeSelection.recentInboundReachable,
63
+ routeReason: args.routeSelection.routeReason,
64
+ mediaUrl: args.mediaUrl,
65
+ };
66
+ }
@@ -0,0 +1,31 @@
1
+ import type { BncrConnection, OutboxEntry } from './types.ts';
2
+
3
+ export function prepareFileTransferRouteSelection(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
+ selectOutboxFileTransferRouteCandidates: (args: {
11
+ routeCandidates: Iterable<string>;
12
+ attemptedConnIds: string[];
13
+ recentInboundConnIds: Iterable<string>;
14
+ ownerConnId?: string;
15
+ recentInboundReachable: boolean;
16
+ isRevalidatedAttemptedConn: (connId: string) => boolean;
17
+ }) => { connIds: Iterable<string>; recentInboundReachable: boolean; routeReason: string };
18
+ }) {
19
+ const attemptedConnIds = Array.isArray(args.entry.routeAttemptConnIds)
20
+ ? args.entry.routeAttemptConnIds.filter((v): v is string => typeof v === 'string' && !!v)
21
+ : [];
22
+
23
+ return args.selectOutboxFileTransferRouteCandidates({
24
+ routeCandidates: args.resolvePushConnIds(args.entry.accountId),
25
+ attemptedConnIds,
26
+ recentInboundConnIds: args.resolveRecentInboundConnIds(args.entry.accountId),
27
+ ownerConnId: args.owner?.connId,
28
+ recentInboundReachable: args.hasRecentInboundReachability(args.entry.accountId),
29
+ isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
30
+ });
31
+ }
@@ -0,0 +1,34 @@
1
+ import type { BncrConnection, OutboxEntry } from './types.ts';
2
+ import {
3
+ buildPushBroadcastPayload,
4
+ buildPushRouteSelectArgs,
5
+ } from './outbox-push-args.ts';
6
+
7
+ export function buildFileTransferBroadcastPayload(args: {
8
+ frame: Record<string, unknown>;
9
+ messageId: string;
10
+ }) {
11
+ return buildPushBroadcastPayload({
12
+ payload: args.frame,
13
+ messageId: args.messageId,
14
+ });
15
+ }
16
+
17
+ export function buildFileTransferRouteSelectArgs(args: {
18
+ entry: OutboxEntry;
19
+ connIds: Iterable<string>;
20
+ routeReason: string;
21
+ recentInboundReachable: boolean;
22
+ owner: BncrConnection | null;
23
+ event: string;
24
+ }) {
25
+ return buildPushRouteSelectArgs({
26
+ entry: args.entry,
27
+ connIds: args.connIds,
28
+ routeReason: args.routeReason,
29
+ recentInboundReachable: args.recentInboundReachable,
30
+ owner: args.owner,
31
+ event: args.event,
32
+ kind: 'file-transfer',
33
+ });
34
+ }
@@ -0,0 +1,67 @@
1
+ import type { BncrConnection, OutboxEntry } from './types.ts';
2
+
3
+ export function buildPushBroadcastPayload(args: {
4
+ payload: Record<string, unknown>;
5
+ messageId: string;
6
+ }) {
7
+ return {
8
+ ...args.payload,
9
+ idempotencyKey: args.messageId,
10
+ };
11
+ }
12
+
13
+ export function buildPushRouteSelectArgs(args: {
14
+ entry: OutboxEntry;
15
+ connIds: Iterable<string>;
16
+ routeReason: string;
17
+ recentInboundReachable: boolean;
18
+ owner: BncrConnection | null;
19
+ event: string;
20
+ kind?: 'file-transfer';
21
+ }) {
22
+ return {
23
+ messageId: args.entry.messageId,
24
+ accountId: args.entry.accountId,
25
+ kind: args.kind,
26
+ routeReason: args.routeReason,
27
+ connIds: args.connIds,
28
+ ownerConnId: args.owner?.connId || '',
29
+ ownerClientId: args.owner?.clientId || '',
30
+ recentInboundReachable: args.recentInboundReachable,
31
+ event: args.event,
32
+ };
33
+ }
34
+
35
+ export function buildPushOkArgs(args: {
36
+ entry: OutboxEntry;
37
+ connIds: Iterable<string>;
38
+ recentInboundReachable: boolean;
39
+ event: string;
40
+ kind?: 'file-transfer';
41
+ }) {
42
+ return {
43
+ messageId: args.entry.messageId,
44
+ accountId: args.entry.accountId,
45
+ kind: args.kind,
46
+ connIds: args.connIds,
47
+ ownerConnId: args.entry.lastPushConnId || '',
48
+ ownerClientId: args.entry.lastPushClientId || '',
49
+ recentInboundReachable: args.recentInboundReachable,
50
+ event: args.event,
51
+ };
52
+ }
53
+
54
+ export function buildPushFailureArgs(args: {
55
+ entry: OutboxEntry;
56
+ retryable?: boolean;
57
+ kind?: 'file-transfer';
58
+ }) {
59
+ return {
60
+ messageId: args.entry.messageId,
61
+ accountId: args.entry.accountId,
62
+ retryCount: args.entry.retryCount,
63
+ kind: args.kind,
64
+ retryable: args.retryable,
65
+ lastError: args.entry.lastError,
66
+ };
67
+ }
@@ -0,0 +1,69 @@
1
+ import type { OutboxEntry } from './types.ts';
2
+
3
+ export function buildDeadLetterEntry(entry: OutboxEntry, reason: string): OutboxEntry {
4
+ return {
5
+ ...entry,
6
+ lastError: reason,
7
+ };
8
+ }
9
+
10
+ export function appendDeadLetter(args: {
11
+ deadLetter: OutboxEntry[];
12
+ entry: OutboxEntry;
13
+ maxEntries: number;
14
+ }): OutboxEntry[] {
15
+ const next = [...args.deadLetter, args.entry];
16
+ if (next.length <= args.maxEntries) return next;
17
+ return next.slice(-args.maxEntries);
18
+ }
19
+
20
+ export function collectDueOutboxEntries(args: {
21
+ outbox: Iterable<OutboxEntry>;
22
+ accountId: string;
23
+ now: number;
24
+ maxBatch: number;
25
+ maxRetry: number;
26
+ backoffMs: (retryCount: number) => number;
27
+ }): {
28
+ duePayloads: Array<Record<string, unknown>>;
29
+ updatedEntries: OutboxEntry[];
30
+ deadLetterEntries: OutboxEntry[];
31
+ } {
32
+ const duePayloads: Array<Record<string, unknown>> = [];
33
+ const updatedEntries: OutboxEntry[] = [];
34
+ const deadLetterEntries: OutboxEntry[] = [];
35
+
36
+ for (const originalEntry of args.outbox) {
37
+ if (originalEntry.accountId !== args.accountId) continue;
38
+ if (originalEntry.nextAttemptAt > args.now) continue;
39
+
40
+ const nextAttempt = originalEntry.retryCount + 1;
41
+ if (nextAttempt > args.maxRetry) {
42
+ deadLetterEntries.push(buildDeadLetterEntry(originalEntry, 'retry-limit'));
43
+ continue;
44
+ }
45
+
46
+ const updatedEntry: OutboxEntry = {
47
+ ...originalEntry,
48
+ retryCount: nextAttempt,
49
+ lastAttemptAt: args.now,
50
+ nextAttemptAt: args.now + args.backoffMs(nextAttempt),
51
+ };
52
+ updatedEntries.push(updatedEntry);
53
+ duePayloads.push({
54
+ ...updatedEntry.payload,
55
+ _meta: {
56
+ retryCount: updatedEntry.retryCount,
57
+ nextAttemptAt: updatedEntry.nextAttemptAt,
58
+ },
59
+ });
60
+
61
+ if (duePayloads.length >= args.maxBatch) break;
62
+ }
63
+
64
+ return {
65
+ duePayloads,
66
+ updatedEntries,
67
+ deadLetterEntries,
68
+ };
69
+ }
@@ -0,0 +1,14 @@
1
+ import type { OutboxEntry } from './types.ts';
2
+
3
+ export function summarizeOutboxEntry(args: {
4
+ entry: OutboxEntry;
5
+ asString: (value: unknown) => string;
6
+ formatDisplayScope: (route: OutboxEntry['route']) => string;
7
+ summarizeTextPreview: (raw: string, limit?: number) => string;
8
+ }) {
9
+ const msg = (args.entry.payload as any)?.message || {};
10
+ const type = args.asString(msg.type || (args.entry.payload as any)?.type || 'unknown');
11
+ const text = args.asString(msg.msg || '');
12
+ const preview = args.summarizeTextPreview(text);
13
+ return [type, args.formatDisplayScope(args.entry.route), preview].join('|');
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { OutboxEntry } from './types.ts';
2
+ import { buildPushFailureArgs } from './outbox-push-args.ts';
3
+
4
+ export function buildTextPushFailureArgs(args: {
5
+ entry: OutboxEntry;
6
+ }) {
7
+ return buildPushFailureArgs({
8
+ entry: args.entry,
9
+ });
10
+ }
@@ -0,0 +1,51 @@
1
+ import type { OutboxEntry } from './types.ts';
2
+
3
+ export type TextPushGuardResult =
4
+ | { ok: false; reason: 'no-gateway-context' }
5
+ | {
6
+ ok: false;
7
+ reason: 'no-active-connection';
8
+ recentInboundReachable: boolean;
9
+ }
10
+ | {
11
+ ok: true;
12
+ connIds: Set<string>;
13
+ recentInboundReachable: boolean;
14
+ routeReason: string;
15
+ ownerConnId?: string;
16
+ };
17
+
18
+ export function resolveTextPushGuard(args: {
19
+ gatewayContext: unknown;
20
+ entry: OutboxEntry;
21
+ routeSelection: {
22
+ connIds: Iterable<string>;
23
+ recentInboundReachable: boolean;
24
+ routeReason: string;
25
+ ownerConnId?: string;
26
+ };
27
+ }): TextPushGuardResult {
28
+ if (!args.gatewayContext) {
29
+ return {
30
+ ok: false,
31
+ reason: 'no-gateway-context',
32
+ };
33
+ }
34
+
35
+ const connIds = new Set(args.routeSelection.connIds);
36
+ if (!connIds.size) {
37
+ return {
38
+ ok: false,
39
+ reason: 'no-active-connection',
40
+ recentInboundReachable: args.routeSelection.recentInboundReachable,
41
+ };
42
+ }
43
+
44
+ return {
45
+ ok: true,
46
+ connIds,
47
+ recentInboundReachable: args.routeSelection.recentInboundReachable,
48
+ routeReason: args.routeSelection.routeReason,
49
+ ownerConnId: args.routeSelection.ownerConnId,
50
+ };
51
+ }
@@ -0,0 +1,36 @@
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
+ }
@@ -0,0 +1,62 @@
1
+ import type { BncrConnection, OutboxEntry } from './types.ts';
2
+ import {
3
+ buildPushBroadcastPayload,
4
+ buildPushOkArgs,
5
+ buildPushRouteSelectArgs,
6
+ } from './outbox-push-args.ts';
7
+
8
+ export function buildTextPushBroadcastPayload(args: {
9
+ payload: Record<string, unknown>;
10
+ messageId: string;
11
+ }) {
12
+ return buildPushBroadcastPayload({
13
+ payload: args.payload,
14
+ messageId: args.messageId,
15
+ });
16
+ }
17
+
18
+ export function buildTextPushRouteSelectArgs(args: {
19
+ entry: OutboxEntry;
20
+ connIds: Iterable<string>;
21
+ routeReason: string;
22
+ recentInboundReachable: boolean;
23
+ owner: BncrConnection | null;
24
+ event: string;
25
+ }) {
26
+ return buildPushRouteSelectArgs({
27
+ entry: args.entry,
28
+ connIds: args.connIds,
29
+ routeReason: args.routeReason,
30
+ recentInboundReachable: args.recentInboundReachable,
31
+ owner: args.owner,
32
+ event: args.event,
33
+ });
34
+ }
35
+
36
+ export function buildTextPushSuccessArgs(args: {
37
+ entry: OutboxEntry;
38
+ connIds: Iterable<string>;
39
+ ownerConnId?: string;
40
+ ownerClientId?: string;
41
+ }) {
42
+ return {
43
+ entry: args.entry,
44
+ connIds: args.connIds,
45
+ ownerConnId: args.ownerConnId,
46
+ ownerClientId: args.ownerClientId,
47
+ };
48
+ }
49
+
50
+ export function buildTextPushOkArgs(args: {
51
+ entry: OutboxEntry;
52
+ connIds: Iterable<string>;
53
+ recentInboundReachable: boolean;
54
+ event: string;
55
+ }) {
56
+ return buildPushOkArgs({
57
+ entry: args.entry,
58
+ connIds: args.connIds,
59
+ recentInboundReachable: args.recentInboundReachable,
60
+ event: args.event,
61
+ });
62
+ }
@@ -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
+ }
@@ -0,0 +1,115 @@
1
+ const DEFAULT_REGISTER_WARMUP_WINDOW_MS = 30_000;
2
+
3
+ function finiteNumberOr(value: unknown, fallback: number): number {
4
+ const n = Number(value);
5
+ return Number.isFinite(n) ? n : fallback;
6
+ }
7
+
8
+ export type RegisterTraceEntry = {
9
+ ts: number;
10
+ bridgeId: string;
11
+ gatewayPid: number;
12
+ registerCount: number;
13
+ apiGeneration: number;
14
+ apiRebound: boolean;
15
+ apiInstanceId: string | null;
16
+ registryFingerprint: string | null;
17
+ source: string | null;
18
+ pluginVersion: string | null;
19
+ stack: string;
20
+ stackBucket: string;
21
+ };
22
+
23
+ export type RegisterTraceSummary = {
24
+ startupWindowMs: number;
25
+ traceWindowSize: number;
26
+ sourceBuckets: Record<string, number>;
27
+ dominantBucket: string | null;
28
+ warmupRegisterCount: number;
29
+ postWarmupRegisterCount: number;
30
+ unexpectedRegisterAfterWarmup: boolean;
31
+ lastUnexpectedRegisterAt: number | null;
32
+ likelyRuntimeRegistryDrift: boolean;
33
+ likelyStartupFanoutOnly: boolean;
34
+ };
35
+
36
+ export function classifyRegisterTrace(stack: string) {
37
+ if (
38
+ stack.includes('prepareSecretsRuntimeSnapshot') ||
39
+ stack.includes('resolveRuntimeWebTools') ||
40
+ stack.includes('resolvePluginWebSearchProviders')
41
+ ) {
42
+ return 'runtime/webtools';
43
+ }
44
+ if (stack.includes('startGatewayServer') || stack.includes('loadGatewayPlugins')) {
45
+ return 'gateway/startup';
46
+ }
47
+ if (stack.includes('resolvePluginImplicitProviders')) {
48
+ return 'provider/discovery/implicit';
49
+ }
50
+ if (stack.includes('resolvePluginDiscoveryProviders')) {
51
+ return 'provider/discovery/discovery';
52
+ }
53
+ if (stack.includes('resolvePluginProviders')) {
54
+ return 'provider/discovery/providers';
55
+ }
56
+ return 'other';
57
+ }
58
+
59
+ export function dominantRegisterBucket(sourceBuckets: Record<string, number>) {
60
+ let winner: string | null = null;
61
+ let winnerCount = -1;
62
+ for (const [bucket, count] of Object.entries(sourceBuckets)) {
63
+ if (count > winnerCount) {
64
+ winner = bucket;
65
+ winnerCount = count;
66
+ }
67
+ }
68
+ return winner;
69
+ }
70
+
71
+ export function buildRegisterTraceSummary(args: {
72
+ traceRecent: RegisterTraceEntry[];
73
+ firstRegisterAt: number | null;
74
+ warmupWindowMs?: number;
75
+ }): RegisterTraceSummary {
76
+ const warmupWindowMs = Math.max(
77
+ 0,
78
+ finiteNumberOr(args.warmupWindowMs, DEFAULT_REGISTER_WARMUP_WINDOW_MS),
79
+ );
80
+ const buckets: Record<string, number> = {};
81
+ let warmupCount = 0;
82
+ let postWarmupCount = 0;
83
+ let unexpectedRegisterAfterWarmup = false;
84
+ let lastUnexpectedRegisterAt: number | null = null;
85
+ const baseline = args.firstRegisterAt;
86
+
87
+ for (const trace of args.traceRecent) {
88
+ buckets[trace.stackBucket] = (buckets[trace.stackBucket] || 0) + 1;
89
+ const isWarmup = baseline != null && trace.ts - baseline <= warmupWindowMs;
90
+ if (isWarmup) {
91
+ warmupCount += 1;
92
+ } else {
93
+ postWarmupCount += 1;
94
+ unexpectedRegisterAfterWarmup = true;
95
+ lastUnexpectedRegisterAt = trace.ts;
96
+ }
97
+ }
98
+
99
+ const dominantBucket = dominantRegisterBucket(buckets);
100
+ const likelyRuntimeRegistryDrift = postWarmupCount > 0;
101
+ const likelyStartupFanoutOnly = warmupCount > 0 && postWarmupCount === 0;
102
+
103
+ return {
104
+ startupWindowMs: warmupWindowMs,
105
+ traceWindowSize: args.traceRecent.length,
106
+ sourceBuckets: buckets,
107
+ dominantBucket,
108
+ warmupRegisterCount: warmupCount,
109
+ postWarmupRegisterCount: postWarmupCount,
110
+ unexpectedRegisterAfterWarmup,
111
+ lastUnexpectedRegisterAt,
112
+ likelyRuntimeRegistryDrift,
113
+ likelyStartupFanoutOnly,
114
+ };
115
+ }
@@ -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);
@@ -142,6 +147,58 @@ export function buildAccountRuntimeSnapshot(input: RuntimeStatusInput) {
142
147
  };
143
148
  }
144
149
 
150
+ export function buildAccountStatusSnapshot(input: {
151
+ account: { accountId: string; name?: string; enabled?: boolean };
152
+ runtime: any;
153
+ healthSummary: string;
154
+ displayName: string;
155
+ }) {
156
+ const rt = input.runtime || {};
157
+ const meta = rt?.meta || {};
158
+
159
+ const pending = finiteNumberOr(rt?.pending ?? meta.pending, 0);
160
+ const deadLetter = finiteNumberOr(rt?.deadLetter ?? meta.deadLetter, 0);
161
+ const lastSessionKey = rt?.lastSessionKey ?? meta.lastSessionKey ?? null;
162
+ const lastSessionScope = rt?.lastSessionScope ?? meta.lastSessionScope ?? null;
163
+ const lastSessionAt = rt?.lastSessionAt ?? meta.lastSessionAt ?? null;
164
+ const lastSessionAgo = rt?.lastSessionAgo ?? meta.lastSessionAgo ?? '-';
165
+ const lastActivityAt = rt?.lastActivityAt ?? meta.lastActivityAt ?? null;
166
+ const lastActivityAgo = rt?.lastActivityAgo ?? meta.lastActivityAgo ?? '-';
167
+ const lastInboundAt = rt?.lastInboundAt ?? meta.lastInboundAt ?? null;
168
+ const lastInboundAgo = rt?.lastInboundAgo ?? meta.lastInboundAgo ?? '-';
169
+ const lastOutboundAt = rt?.lastOutboundAt ?? meta.lastOutboundAt ?? null;
170
+ const lastOutboundAgo = rt?.lastOutboundAgo ?? meta.lastOutboundAgo ?? '-';
171
+ const diagnostics = rt?.diagnostics ?? meta.diagnostics ?? null;
172
+ const normalizedMode = rt?.mode === 'linked' ? 'linked' : 'Status';
173
+
174
+ return {
175
+ accountId: input.account.accountId,
176
+ name: input.displayName,
177
+ enabled: input.account.enabled !== false,
178
+ configured: true,
179
+ linked: Boolean(rt?.connected),
180
+ running: rt?.running ?? false,
181
+ connected: rt?.connected ?? false,
182
+ lastEventAt: rt?.lastEventAt ?? null,
183
+ lastError: rt?.lastError ?? null,
184
+ mode: normalizedMode,
185
+ pending,
186
+ deadLetter,
187
+ healthSummary: input.healthSummary,
188
+ lastSessionKey,
189
+ lastSessionScope,
190
+ lastSessionAt,
191
+ lastSessionAgo,
192
+ lastActivityAt,
193
+ lastActivityAgo,
194
+ lastInboundAt,
195
+ lastInboundAgo,
196
+ lastOutboundAt,
197
+ lastOutboundAgo,
198
+ diagnostics,
199
+ };
200
+ }
201
+
145
202
  export function buildChannelSummaryFromRuntime(input: RuntimeStatusInput) {
146
203
  const headline = buildStatusHeadlineFromRuntime(input);
147
204
  return {
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 = {