@xmoxmo/bncr 0.2.3 → 0.2.5

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 (36) hide show
  1. package/README.md +67 -4
  2. package/index.ts +24 -1
  3. package/package.json +1 -1
  4. package/src/channel.ts +2823 -1178
  5. package/src/core/connection-capability.ts +70 -0
  6. package/src/core/connection-reachability.ts +141 -0
  7. package/src/core/diagnostics.ts +49 -0
  8. package/src/core/downlink-health.ts +56 -0
  9. package/src/core/extended-diagnostics.ts +65 -0
  10. package/src/core/lease-state.ts +94 -0
  11. package/src/core/outbox-enqueue.ts +22 -0
  12. package/src/core/outbox-entry-builders.ts +91 -0
  13. package/src/core/outbox-file-transfer-bookkeeping.ts +31 -0
  14. package/src/core/outbox-file-transfer-failure.ts +25 -0
  15. package/src/core/outbox-file-transfer-guards.ts +66 -0
  16. package/src/core/outbox-file-transfer-prep.ts +31 -0
  17. package/src/core/outbox-file-transfer-success.ts +34 -0
  18. package/src/core/outbox-push-args.ts +67 -0
  19. package/src/core/outbox-queue.ts +69 -0
  20. package/src/core/outbox-summary.ts +14 -0
  21. package/src/core/outbox-text-push-failure.ts +10 -0
  22. package/src/core/outbox-text-push-guards.ts +51 -0
  23. package/src/core/outbox-text-push-prep.ts +36 -0
  24. package/src/core/outbox-text-push-success.ts +62 -0
  25. package/src/core/register-trace.ts +110 -0
  26. package/src/core/status.ts +62 -10
  27. package/src/core/types.ts +3 -0
  28. package/src/messaging/inbound/dispatch.ts +86 -48
  29. package/src/messaging/outbound/diagnostics.ts +246 -0
  30. package/src/messaging/outbound/media-dedupe.ts +51 -0
  31. package/src/messaging/outbound/queue-selectors.ts +186 -0
  32. package/src/messaging/outbound/reasons.ts +48 -0
  33. package/src/messaging/outbound/reply-enqueue.ts +329 -0
  34. package/src/messaging/outbound/retry-policy.ts +133 -0
  35. package/src/messaging/outbound/send.ts +2 -0
  36. 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
+ }
@@ -0,0 +1,110 @@
1
+ const DEFAULT_REGISTER_WARMUP_WINDOW_MS = 30_000;
2
+
3
+ export type RegisterTraceEntry = {
4
+ ts: number;
5
+ bridgeId: string;
6
+ gatewayPid: number;
7
+ registerCount: number;
8
+ apiGeneration: number;
9
+ apiRebound: boolean;
10
+ apiInstanceId: string | null;
11
+ registryFingerprint: string | null;
12
+ source: string | null;
13
+ pluginVersion: string | null;
14
+ stack: string;
15
+ stackBucket: string;
16
+ };
17
+
18
+ export type RegisterTraceSummary = {
19
+ startupWindowMs: number;
20
+ traceWindowSize: number;
21
+ sourceBuckets: Record<string, number>;
22
+ dominantBucket: string | null;
23
+ warmupRegisterCount: number;
24
+ postWarmupRegisterCount: number;
25
+ unexpectedRegisterAfterWarmup: boolean;
26
+ lastUnexpectedRegisterAt: number | null;
27
+ likelyRuntimeRegistryDrift: boolean;
28
+ likelyStartupFanoutOnly: boolean;
29
+ };
30
+
31
+ export function classifyRegisterTrace(stack: string) {
32
+ if (
33
+ stack.includes('prepareSecretsRuntimeSnapshot') ||
34
+ stack.includes('resolveRuntimeWebTools') ||
35
+ stack.includes('resolvePluginWebSearchProviders')
36
+ ) {
37
+ return 'runtime/webtools';
38
+ }
39
+ if (stack.includes('startGatewayServer') || stack.includes('loadGatewayPlugins')) {
40
+ return 'gateway/startup';
41
+ }
42
+ if (stack.includes('resolvePluginImplicitProviders')) {
43
+ return 'provider/discovery/implicit';
44
+ }
45
+ if (stack.includes('resolvePluginDiscoveryProviders')) {
46
+ return 'provider/discovery/discovery';
47
+ }
48
+ if (stack.includes('resolvePluginProviders')) {
49
+ return 'provider/discovery/providers';
50
+ }
51
+ return 'other';
52
+ }
53
+
54
+ export function dominantRegisterBucket(sourceBuckets: Record<string, number>) {
55
+ let winner: string | null = null;
56
+ let winnerCount = -1;
57
+ for (const [bucket, count] of Object.entries(sourceBuckets)) {
58
+ if (count > winnerCount) {
59
+ winner = bucket;
60
+ winnerCount = count;
61
+ }
62
+ }
63
+ return winner;
64
+ }
65
+
66
+ export function buildRegisterTraceSummary(args: {
67
+ traceRecent: RegisterTraceEntry[];
68
+ firstRegisterAt: number | null;
69
+ warmupWindowMs?: number;
70
+ }): RegisterTraceSummary {
71
+ const warmupWindowMs = Math.max(
72
+ 0,
73
+ Number(args.warmupWindowMs ?? DEFAULT_REGISTER_WARMUP_WINDOW_MS) || 0,
74
+ );
75
+ const buckets: Record<string, number> = {};
76
+ let warmupCount = 0;
77
+ let postWarmupCount = 0;
78
+ let unexpectedRegisterAfterWarmup = false;
79
+ let lastUnexpectedRegisterAt: number | null = null;
80
+ const baseline = args.firstRegisterAt;
81
+
82
+ for (const trace of args.traceRecent) {
83
+ buckets[trace.stackBucket] = (buckets[trace.stackBucket] || 0) + 1;
84
+ const isWarmup = baseline != null && trace.ts - baseline <= warmupWindowMs;
85
+ if (isWarmup) {
86
+ warmupCount += 1;
87
+ } else {
88
+ postWarmupCount += 1;
89
+ unexpectedRegisterAfterWarmup = true;
90
+ lastUnexpectedRegisterAt = trace.ts;
91
+ }
92
+ }
93
+
94
+ const dominantBucket = dominantRegisterBucket(buckets);
95
+ const likelyRuntimeRegistryDrift = postWarmupCount > 0;
96
+ const likelyStartupFanoutOnly = warmupCount > 0 && postWarmupCount === 0;
97
+
98
+ return {
99
+ startupWindowMs: warmupWindowMs,
100
+ traceWindowSize: args.traceRecent.length,
101
+ sourceBuckets: buckets,
102
+ dominantBucket,
103
+ warmupRegisterCount: warmupCount,
104
+ postWarmupRegisterCount: postWarmupCount,
105
+ unexpectedRegisterAfterWarmup,
106
+ lastUnexpectedRegisterAt,
107
+ likelyRuntimeRegistryDrift,
108
+ likelyStartupFanoutOnly,
109
+ };
110
+ }
@@ -75,20 +75,12 @@ export function buildIntegratedDiagnostics(input: RuntimeStatusInput): BncrDiagn
75
75
  export function buildStatusHeadlineFromRuntime(input: RuntimeStatusInput): string {
76
76
  const diag = buildIntegratedDiagnostics(input);
77
77
  const h = diag.health;
78
- const r = diag.regression;
79
-
80
78
  const parts = [
81
- r.ok ? 'diag:ok' : 'diag:warn',
79
+ input.connected ? 'linked' : 'status',
82
80
  `p:${h.pending}`,
83
81
  `d:${h.deadLetter}`,
84
82
  `c:${h.activeConnections}`,
85
83
  ];
86
-
87
- if (!r.ok) {
88
- if (r.invalidOutboxSessionKeys > 0) parts.push(`invalid:${r.invalidOutboxSessionKeys}`);
89
- if (r.legacyAccountResidue > 0) parts.push(`legacy:${r.legacyAccountResidue}`);
90
- }
91
-
92
84
  return parts.join(' ');
93
85
  }
94
86
 
@@ -128,6 +120,7 @@ export function buildStatusMetaFromRuntime(input: RuntimeStatusInput) {
128
120
  }
129
121
 
130
122
  export function buildAccountRuntimeSnapshot(input: RuntimeStatusInput) {
123
+ const meta = buildStatusMetaFromRuntime(input);
131
124
  return {
132
125
  accountId: input.accountId,
133
126
  running: input.running ?? true,
@@ -138,7 +131,66 @@ export function buildAccountRuntimeSnapshot(input: RuntimeStatusInput) {
138
131
  lastOutboundAt: input.lastOutboundAt || null,
139
132
  mode: input.connected ? 'linked' : 'configured',
140
133
  lastError: input.lastError ?? null,
141
- meta: buildStatusMetaFromRuntime(input),
134
+ pending: input.pending,
135
+ deadLetter: input.deadLetter,
136
+ lastSessionKey: input.lastSession?.sessionKey || null,
137
+ lastSessionScope: input.lastSession?.scope || null,
138
+ lastSessionAt: input.lastSession?.updatedAt || null,
139
+ lastActivityAt: input.lastActivityAt || null,
140
+ diagnostics: meta.diagnostics,
141
+ meta,
142
+ };
143
+ }
144
+
145
+ export function buildAccountStatusSnapshot(input: {
146
+ account: { accountId: string; name?: string; enabled?: boolean };
147
+ runtime: any;
148
+ healthSummary: string;
149
+ displayName: string;
150
+ }) {
151
+ const rt = input.runtime || {};
152
+ const meta = rt?.meta || {};
153
+
154
+ const pending = Number(rt?.pending ?? meta.pending ?? 0);
155
+ const deadLetter = Number(rt?.deadLetter ?? meta.deadLetter ?? 0);
156
+ const lastSessionKey = rt?.lastSessionKey ?? meta.lastSessionKey ?? null;
157
+ const lastSessionScope = rt?.lastSessionScope ?? meta.lastSessionScope ?? null;
158
+ const lastSessionAt = rt?.lastSessionAt ?? meta.lastSessionAt ?? null;
159
+ const lastSessionAgo = rt?.lastSessionAgo ?? meta.lastSessionAgo ?? '-';
160
+ const lastActivityAt = rt?.lastActivityAt ?? meta.lastActivityAt ?? null;
161
+ const lastActivityAgo = rt?.lastActivityAgo ?? meta.lastActivityAgo ?? '-';
162
+ const lastInboundAt = rt?.lastInboundAt ?? meta.lastInboundAt ?? null;
163
+ const lastInboundAgo = rt?.lastInboundAgo ?? meta.lastInboundAgo ?? '-';
164
+ const lastOutboundAt = rt?.lastOutboundAt ?? meta.lastOutboundAt ?? null;
165
+ const lastOutboundAgo = rt?.lastOutboundAgo ?? meta.lastOutboundAgo ?? '-';
166
+ const diagnostics = rt?.diagnostics ?? meta.diagnostics ?? null;
167
+ const normalizedMode = rt?.mode === 'linked' ? 'linked' : 'Status';
168
+
169
+ return {
170
+ accountId: input.account.accountId,
171
+ name: input.displayName,
172
+ enabled: input.account.enabled !== false,
173
+ configured: true,
174
+ linked: Boolean(rt?.connected),
175
+ running: rt?.running ?? false,
176
+ connected: rt?.connected ?? false,
177
+ lastEventAt: rt?.lastEventAt ?? null,
178
+ lastError: rt?.lastError ?? null,
179
+ mode: normalizedMode,
180
+ pending,
181
+ deadLetter,
182
+ healthSummary: input.healthSummary,
183
+ lastSessionKey,
184
+ lastSessionScope,
185
+ lastSessionAt,
186
+ lastSessionAgo,
187
+ lastActivityAt,
188
+ lastActivityAgo,
189
+ lastInboundAt,
190
+ lastInboundAgo,
191
+ lastOutboundAt,
192
+ lastOutboundAgo,
193
+ diagnostics,
142
194
  };
143
195
  }
144
196
 
package/src/core/types.ts CHANGED
@@ -49,6 +49,9 @@ export type OutboxEntry = {
49
49
  lastPushAt?: number;
50
50
  lastPushConnId?: string;
51
51
  lastPushClientId?: string;
52
+ routeAttemptConnIds?: string[];
53
+ routeAttemptRound?: number;
54
+ fastReroutePending?: boolean;
52
55
  };
53
56
 
54
57
  export type BncrDiagnosticsSummary = {