@beeos-ai/device-agent 0.9.3 → 0.9.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.
- package/dist/adapters/transport-bridge-ws.d.ts +12 -0
- package/dist/adapters/transport-bridge-ws.js +27 -3
- package/dist/adapters/transport-bridge-ws.js.map +1 -1
- package/dist/agent-runtime/core.d.ts +1 -1
- package/dist/agent-runtime/core.js +41 -0
- package/dist/agent-runtime/core.js.map +1 -1
- package/dist/app/session-manager.d.ts +14 -11
- package/dist/app/session-manager.js +8 -9
- package/dist/app/session-manager.js.map +1 -1
- package/dist/bootstrap.js +140 -11
- package/dist/bootstrap.js.map +1 -1
- package/dist/chat.js +11 -1
- package/dist/chat.js.map +1 -1
- package/dist/config/loader.js +3 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +3 -0
- package/dist/config/schema.js +51 -15
- package/dist/config/schema.js.map +1 -1
- package/dist/fleet/adapters/process/node-process-manager.d.ts +115 -13
- package/dist/fleet/adapters/process/node-process-manager.js +412 -64
- package/dist/fleet/adapters/process/node-process-manager.js.map +1 -1
- package/dist/fleet/adapters/state/port-allocator.d.ts +12 -0
- package/dist/fleet/adapters/state/port-allocator.js +39 -5
- package/dist/fleet/adapters/state/port-allocator.js.map +1 -1
- package/dist/fleet/domain/events.d.ts +23 -1
- package/dist/fleet/domain/fsm.d.ts +13 -1
- package/dist/fleet/domain/fsm.js +70 -0
- package/dist/fleet/domain/fsm.js.map +1 -1
- package/dist/fleet/supervisor.js +30 -0
- package/dist/fleet/supervisor.js.map +1 -1
- package/dist/health/adb-track-devices.d.ts +103 -0
- package/dist/health/adb-track-devices.js +171 -0
- package/dist/health/adb-track-devices.js.map +1 -0
- package/dist/health/health-reporter.d.ts +67 -0
- package/dist/health/health-reporter.js +123 -0
- package/dist/health/health-reporter.js.map +1 -0
- package/dist/health/index.d.ts +6 -0
- package/dist/health/index.js +4 -0
- package/dist/health/index.js.map +1 -0
- package/dist/health/mcp-health-monitor.d.ts +190 -0
- package/dist/health/mcp-health-monitor.js +267 -0
- package/dist/health/mcp-health-monitor.js.map +1 -0
- package/dist/inbound/cli/main.js +8 -1
- package/dist/inbound/cli/main.js.map +1 -1
- package/dist/inbound/cli/run-command.js +11 -1
- package/dist/inbound/cli/run-command.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-client/action-mapping.d.ts +45 -0
- package/dist/mcp-client/action-mapping.js +224 -0
- package/dist/mcp-client/action-mapping.js.map +1 -0
- package/dist/mcp-client/autospawn.d.ts +15 -2
- package/dist/mcp-client/autospawn.js +82 -4
- package/dist/mcp-client/autospawn.js.map +1 -1
- package/dist/mcp-client/client.d.ts +134 -38
- package/dist/mcp-client/client.js +394 -87
- package/dist/mcp-client/client.js.map +1 -1
- package/dist/mcp-client/index.d.ts +5 -3
- package/dist/mcp-client/index.js +2 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/inproc.d.ts +2 -2
- package/dist/mcp-client/inproc.js.map +1 -1
- package/dist/mcp-client/runtime.d.ts +124 -21
- package/dist/mcp-client/runtime.js +18 -7
- package/dist/mcp-client/runtime.js.map +1 -1
- package/dist/model/adapters/autoglm.js +44 -45
- package/dist/model/adapters/autoglm.js.map +1 -1
- package/dist/plugins/acp/index.d.ts +35 -0
- package/dist/plugins/acp/index.js +121 -0
- package/dist/plugins/acp/index.js.map +1 -1
- package/dist/ports/transport.d.ts +11 -0
- package/package.json +3 -2
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdbTrackDevices — push-source for MCPHealthMonitor.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to `adb track-devices` events for a single target serial and
|
|
5
|
+
* emits a debounced `change` event whenever the device's authoritative
|
|
6
|
+
* ADB state transitions. Wired into MCPHealthMonitor in the ACP plugin so
|
|
7
|
+
* USB unplug events propagate to the cluster within ~2-3s instead of
|
|
8
|
+
* waiting for the 60s active probe / 5s flap window.
|
|
9
|
+
*
|
|
10
|
+
* Why a thin wrapper around fleet's LocalAdbSource?
|
|
11
|
+
* The fleet supervisor already implements a complete `adb track-devices`
|
|
12
|
+
* client (frame parser, retry with backoff, snapshot diffing). Spawning
|
|
13
|
+
* a second client from inside device-agent (when running under fleet)
|
|
14
|
+
* wastes one process per device, but the alternative — fleet → child
|
|
15
|
+
* IPC — is materially more complex (~170 lines vs ~80 lines for double-
|
|
16
|
+
* spawn). Per-device overhead is < 5 MB; the adb daemon supports
|
|
17
|
+
* multiple concurrent track-devices clients without coordination, and
|
|
18
|
+
* keeping the two stacks independent gives us fault isolation (if fleet
|
|
19
|
+
* crashes, device-agent still detects the disconnect).
|
|
20
|
+
*
|
|
21
|
+
* Asymmetric debounce (KISS-derived from real UX requirements):
|
|
22
|
+
* - Non-healthy states (offline / unauthorized / absent) → 2000 ms
|
|
23
|
+
* trailing debounce. Covers typical USB jitter (cable wiggle, hub
|
|
24
|
+
* re-enumeration, ADB auth race) without spamming the control plane.
|
|
25
|
+
* Each new observation cancels the pending emission, so a single
|
|
26
|
+
* short hiccup never reaches health-reporter.
|
|
27
|
+
* - Healthy state (device) → emitted immediately (0 ms). The user is
|
|
28
|
+
* actively waiting for "I plugged back in, did it work?" feedback, so
|
|
29
|
+
* any delay is a UX regression. Emitting healthy also cancels any
|
|
30
|
+
* pending offline emission so post-jitter recovery doesn't briefly
|
|
31
|
+
* report disconnect.
|
|
32
|
+
*
|
|
33
|
+
* Both values are module-level constants (YAGNI — no env override until
|
|
34
|
+
* a real user reports their hub jitters longer than 2s; tests inject
|
|
35
|
+
* mock LocalAdbSource events directly).
|
|
36
|
+
*/
|
|
37
|
+
import { LocalAdbSource } from "../fleet/adapters/sources/local-adb.js";
|
|
38
|
+
import { EventEmitter } from "node:events";
|
|
39
|
+
import type { Logger } from "pino";
|
|
40
|
+
export type AdbHealthState = {
|
|
41
|
+
ok: true;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
reason: "device_disconnected" | "device_unauthorized";
|
|
45
|
+
};
|
|
46
|
+
export interface AdbTrackDevicesOptions {
|
|
47
|
+
/** Target serial — only emit for this serial. Required. */
|
|
48
|
+
targetSerial: string;
|
|
49
|
+
logger: Logger;
|
|
50
|
+
/**
|
|
51
|
+
* Test seam — inject a custom IDeviceSource (e.g. a mock that exposes
|
|
52
|
+
* `emit`) instead of spawning a real `adb track-devices`. Production
|
|
53
|
+
* callers omit and get the default LocalAdbSource.
|
|
54
|
+
*/
|
|
55
|
+
source?: ConstructorOf<LocalAdbSource> | LocalAdbSource;
|
|
56
|
+
}
|
|
57
|
+
type ConstructorOf<T> = new (...args: never[]) => T;
|
|
58
|
+
export declare class AdbTrackDevices extends EventEmitter {
|
|
59
|
+
private readonly targetSerial;
|
|
60
|
+
private readonly logger;
|
|
61
|
+
private readonly source;
|
|
62
|
+
private unsubscribe;
|
|
63
|
+
private pendingTimer;
|
|
64
|
+
private lastEmitted;
|
|
65
|
+
private currentObserved;
|
|
66
|
+
private disposed;
|
|
67
|
+
constructor(opts: AdbTrackDevicesOptions);
|
|
68
|
+
/**
|
|
69
|
+
* Begin observing. Idempotent. If the underlying `adb` command can't be
|
|
70
|
+
* spawned, LocalAdbSource will retry with exponential backoff — we
|
|
71
|
+
* surface that via log only; no `unavailable` event because the source
|
|
72
|
+
* may eventually recover.
|
|
73
|
+
*/
|
|
74
|
+
start(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Tear down: unsubscribe from source, stop the source, clear any pending
|
|
77
|
+
* debounce timer. Safe to call multiple times.
|
|
78
|
+
*/
|
|
79
|
+
dispose(): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Translate an AdbEvent into the AdbHealthState shape and feed it
|
|
82
|
+
* through the asymmetric debounce machinery.
|
|
83
|
+
*
|
|
84
|
+
* Mapping:
|
|
85
|
+
* adb_online → ok: true (immediate emit)
|
|
86
|
+
* adb_unauthorized → ok: false / unauth (2s debounce — user may
|
|
87
|
+
* still tap "Allow")
|
|
88
|
+
* adb_offline → ok: false / discon (2s debounce — USB jitter)
|
|
89
|
+
*/
|
|
90
|
+
private observeEvent;
|
|
91
|
+
/**
|
|
92
|
+
* Asymmetric trailing debounce.
|
|
93
|
+
* - If `next` is unchanged from `lastEmitted`, no-op.
|
|
94
|
+
* - Always cancel any pending timer (a new observation supersedes a
|
|
95
|
+
* previously scheduled emit — this is what kills false alarms
|
|
96
|
+
* during sub-2s jitter).
|
|
97
|
+
* - Healthy: emit immediately (HEALTHY_DEBOUNCE_MS=0).
|
|
98
|
+
* - Unhealthy: schedule emit after OFFLINE_DEBOUNCE_MS=2000.
|
|
99
|
+
*/
|
|
100
|
+
private scheduleEmit;
|
|
101
|
+
private emitNow;
|
|
102
|
+
}
|
|
103
|
+
export {};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdbTrackDevices — push-source for MCPHealthMonitor.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to `adb track-devices` events for a single target serial and
|
|
5
|
+
* emits a debounced `change` event whenever the device's authoritative
|
|
6
|
+
* ADB state transitions. Wired into MCPHealthMonitor in the ACP plugin so
|
|
7
|
+
* USB unplug events propagate to the cluster within ~2-3s instead of
|
|
8
|
+
* waiting for the 60s active probe / 5s flap window.
|
|
9
|
+
*
|
|
10
|
+
* Why a thin wrapper around fleet's LocalAdbSource?
|
|
11
|
+
* The fleet supervisor already implements a complete `adb track-devices`
|
|
12
|
+
* client (frame parser, retry with backoff, snapshot diffing). Spawning
|
|
13
|
+
* a second client from inside device-agent (when running under fleet)
|
|
14
|
+
* wastes one process per device, but the alternative — fleet → child
|
|
15
|
+
* IPC — is materially more complex (~170 lines vs ~80 lines for double-
|
|
16
|
+
* spawn). Per-device overhead is < 5 MB; the adb daemon supports
|
|
17
|
+
* multiple concurrent track-devices clients without coordination, and
|
|
18
|
+
* keeping the two stacks independent gives us fault isolation (if fleet
|
|
19
|
+
* crashes, device-agent still detects the disconnect).
|
|
20
|
+
*
|
|
21
|
+
* Asymmetric debounce (KISS-derived from real UX requirements):
|
|
22
|
+
* - Non-healthy states (offline / unauthorized / absent) → 2000 ms
|
|
23
|
+
* trailing debounce. Covers typical USB jitter (cable wiggle, hub
|
|
24
|
+
* re-enumeration, ADB auth race) without spamming the control plane.
|
|
25
|
+
* Each new observation cancels the pending emission, so a single
|
|
26
|
+
* short hiccup never reaches health-reporter.
|
|
27
|
+
* - Healthy state (device) → emitted immediately (0 ms). The user is
|
|
28
|
+
* actively waiting for "I plugged back in, did it work?" feedback, so
|
|
29
|
+
* any delay is a UX regression. Emitting healthy also cancels any
|
|
30
|
+
* pending offline emission so post-jitter recovery doesn't briefly
|
|
31
|
+
* report disconnect.
|
|
32
|
+
*
|
|
33
|
+
* Both values are module-level constants (YAGNI — no env override until
|
|
34
|
+
* a real user reports their hub jitters longer than 2s; tests inject
|
|
35
|
+
* mock LocalAdbSource events directly).
|
|
36
|
+
*/
|
|
37
|
+
import { LocalAdbSource } from "../fleet/adapters/sources/local-adb.js";
|
|
38
|
+
import { EventEmitter } from "node:events";
|
|
39
|
+
const OFFLINE_DEBOUNCE_MS = 2000;
|
|
40
|
+
const HEALTHY_DEBOUNCE_MS = 0;
|
|
41
|
+
export class AdbTrackDevices extends EventEmitter {
|
|
42
|
+
targetSerial;
|
|
43
|
+
logger;
|
|
44
|
+
source;
|
|
45
|
+
unsubscribe = null;
|
|
46
|
+
pendingTimer = null;
|
|
47
|
+
lastEmitted = null;
|
|
48
|
+
currentObserved = null;
|
|
49
|
+
disposed = false;
|
|
50
|
+
constructor(opts) {
|
|
51
|
+
super();
|
|
52
|
+
this.targetSerial = opts.targetSerial;
|
|
53
|
+
this.logger = opts.logger;
|
|
54
|
+
// The injected `source` is for tests; production path constructs a
|
|
55
|
+
// fresh LocalAdbSource so each device-agent process has its own
|
|
56
|
+
// `adb track-devices` subprocess (per the fault-isolation rationale
|
|
57
|
+
// in the file header).
|
|
58
|
+
this.source =
|
|
59
|
+
opts.source instanceof LocalAdbSource
|
|
60
|
+
? opts.source
|
|
61
|
+
: new LocalAdbSource({
|
|
62
|
+
logger: {
|
|
63
|
+
debug: (msg, data) => this.logger.debug(data ?? {}, msg),
|
|
64
|
+
info: (msg, data) => this.logger.info(data ?? {}, msg),
|
|
65
|
+
warn: (msg, data) => this.logger.warn(data ?? {}, msg),
|
|
66
|
+
error: (msg, data) => this.logger.error(data ?? {}, msg),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Begin observing. Idempotent. If the underlying `adb` command can't be
|
|
72
|
+
* spawned, LocalAdbSource will retry with exponential backoff — we
|
|
73
|
+
* surface that via log only; no `unavailable` event because the source
|
|
74
|
+
* may eventually recover.
|
|
75
|
+
*/
|
|
76
|
+
async start() {
|
|
77
|
+
if (this.disposed || this.unsubscribe)
|
|
78
|
+
return;
|
|
79
|
+
this.unsubscribe = this.source.on((event) => {
|
|
80
|
+
if (event.serial !== this.targetSerial)
|
|
81
|
+
return;
|
|
82
|
+
this.observeEvent(event);
|
|
83
|
+
});
|
|
84
|
+
await this.source.start();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Tear down: unsubscribe from source, stop the source, clear any pending
|
|
88
|
+
* debounce timer. Safe to call multiple times.
|
|
89
|
+
*/
|
|
90
|
+
async dispose() {
|
|
91
|
+
if (this.disposed)
|
|
92
|
+
return;
|
|
93
|
+
this.disposed = true;
|
|
94
|
+
if (this.pendingTimer) {
|
|
95
|
+
clearTimeout(this.pendingTimer);
|
|
96
|
+
this.pendingTimer = null;
|
|
97
|
+
}
|
|
98
|
+
if (this.unsubscribe) {
|
|
99
|
+
this.unsubscribe();
|
|
100
|
+
this.unsubscribe = null;
|
|
101
|
+
}
|
|
102
|
+
await this.source.stop();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Translate an AdbEvent into the AdbHealthState shape and feed it
|
|
106
|
+
* through the asymmetric debounce machinery.
|
|
107
|
+
*
|
|
108
|
+
* Mapping:
|
|
109
|
+
* adb_online → ok: true (immediate emit)
|
|
110
|
+
* adb_unauthorized → ok: false / unauth (2s debounce — user may
|
|
111
|
+
* still tap "Allow")
|
|
112
|
+
* adb_offline → ok: false / discon (2s debounce — USB jitter)
|
|
113
|
+
*/
|
|
114
|
+
observeEvent(event) {
|
|
115
|
+
const next = event.kind === "adb_online"
|
|
116
|
+
? { ok: true }
|
|
117
|
+
: event.kind === "adb_unauthorized"
|
|
118
|
+
? { ok: false, reason: "device_unauthorized" }
|
|
119
|
+
: { ok: false, reason: "device_disconnected" };
|
|
120
|
+
this.scheduleEmit(next);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Asymmetric trailing debounce.
|
|
124
|
+
* - If `next` is unchanged from `lastEmitted`, no-op.
|
|
125
|
+
* - Always cancel any pending timer (a new observation supersedes a
|
|
126
|
+
* previously scheduled emit — this is what kills false alarms
|
|
127
|
+
* during sub-2s jitter).
|
|
128
|
+
* - Healthy: emit immediately (HEALTHY_DEBOUNCE_MS=0).
|
|
129
|
+
* - Unhealthy: schedule emit after OFFLINE_DEBOUNCE_MS=2000.
|
|
130
|
+
*/
|
|
131
|
+
scheduleEmit(next) {
|
|
132
|
+
if (this.disposed)
|
|
133
|
+
return;
|
|
134
|
+
this.currentObserved = next;
|
|
135
|
+
if (this.pendingTimer) {
|
|
136
|
+
clearTimeout(this.pendingTimer);
|
|
137
|
+
this.pendingTimer = null;
|
|
138
|
+
}
|
|
139
|
+
if (sameState(this.lastEmitted, next))
|
|
140
|
+
return;
|
|
141
|
+
const debounceMs = next.ok ? HEALTHY_DEBOUNCE_MS : OFFLINE_DEBOUNCE_MS;
|
|
142
|
+
if (debounceMs === 0) {
|
|
143
|
+
this.emitNow(next);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.pendingTimer = setTimeout(() => {
|
|
147
|
+
this.pendingTimer = null;
|
|
148
|
+
// Re-check `currentObserved` — by the time the timer fires the
|
|
149
|
+
// observed state may have flipped back. Always emit the latest
|
|
150
|
+
// truth, not the value captured when the timer was scheduled.
|
|
151
|
+
if (this.currentObserved && !sameState(this.lastEmitted, this.currentObserved)) {
|
|
152
|
+
this.emitNow(this.currentObserved);
|
|
153
|
+
}
|
|
154
|
+
}, debounceMs);
|
|
155
|
+
}
|
|
156
|
+
emitNow(next) {
|
|
157
|
+
this.lastEmitted = next;
|
|
158
|
+
this.logger.debug({ state: next }, "adb-track-devices: emitted change");
|
|
159
|
+
this.emit("change", next);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function sameState(a, b) {
|
|
163
|
+
if (!a)
|
|
164
|
+
return false;
|
|
165
|
+
if (a.ok !== b.ok)
|
|
166
|
+
return false;
|
|
167
|
+
if (!a.ok && !b.ok)
|
|
168
|
+
return a.reason === b.reason;
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=adb-track-devices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adb-track-devices.js","sourceRoot":"","sources":["../../src/health/adb-track-devices.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HealthReporter — bridges MCPHealthMonitor state changes onto the
|
|
3
|
+
* Bridge ACP transport as `device/health` notifications, and re-emits
|
|
4
|
+
* the last-known state on every transport reconnect.
|
|
5
|
+
*
|
|
6
|
+
* Why the reconnect re-emit?
|
|
7
|
+
* The bridge-client SDK auto-reconnects on transport loss, but its
|
|
8
|
+
* `send()` quietly drops messages while disconnected (`.catch(()
|
|
9
|
+
* => undefined)` in the inbound notifier). Without a reconnect-time
|
|
10
|
+
* replay, a transition that fires while the WebSocket is down is
|
|
11
|
+
* lost forever — and the cluster's view of `connectivity_reason` is
|
|
12
|
+
* stuck on the prior value. The "connected" event handler here
|
|
13
|
+
* makes every reconnect an opportunity to re-sync server-side state.
|
|
14
|
+
*
|
|
15
|
+
* trace_id:
|
|
16
|
+
* Every emission generates a fresh UUID. Logged here, propagated
|
|
17
|
+
* verbatim by Bridge → BridgeHub → Cluster (via the new
|
|
18
|
+
* `connectivity_reason` field carrying it through), and surfaced in
|
|
19
|
+
* each hop's structured logs. On-call can grep one ID and see the
|
|
20
|
+
* whole chain.
|
|
21
|
+
*
|
|
22
|
+
* Lifecycle:
|
|
23
|
+
* `dispose()` removes both the monitor `change` listener and the
|
|
24
|
+
* transport `connected` listener — the IBridgeTransport now exposes
|
|
25
|
+
* `off()` precisely so this reporter can clean up symmetrically.
|
|
26
|
+
*/
|
|
27
|
+
import type { Logger } from "pino";
|
|
28
|
+
import type { MCPHealthMonitor } from "./mcp-health-monitor.js";
|
|
29
|
+
import type { IBridgeTransport } from "../ports/transport.js";
|
|
30
|
+
export interface HealthReporterOptions {
|
|
31
|
+
monitor: MCPHealthMonitor;
|
|
32
|
+
transport: IBridgeTransport;
|
|
33
|
+
logger: Logger;
|
|
34
|
+
/** Override UUID generator for tests. */
|
|
35
|
+
generateTraceId?: () => string;
|
|
36
|
+
}
|
|
37
|
+
export declare class HealthReporter {
|
|
38
|
+
private readonly monitor;
|
|
39
|
+
private readonly transport;
|
|
40
|
+
private readonly logger;
|
|
41
|
+
private readonly generateTraceId;
|
|
42
|
+
private readonly onChange;
|
|
43
|
+
private readonly onConnected;
|
|
44
|
+
/** Last reported state. Null until the first `change` event fires. */
|
|
45
|
+
private last;
|
|
46
|
+
private disposed;
|
|
47
|
+
constructor(opts: HealthReporterOptions);
|
|
48
|
+
/**
|
|
49
|
+
* Edge-triggered emission: only sends when the new state differs from
|
|
50
|
+
* the last reported one. Suppresses redundant traffic from the
|
|
51
|
+
* underlying monitor's idempotent re-fires (the monitor itself does
|
|
52
|
+
* the same suppression, but the double-check here makes the reporter
|
|
53
|
+
* safe to drive from any other source as well).
|
|
54
|
+
*/
|
|
55
|
+
private maybeReport;
|
|
56
|
+
/**
|
|
57
|
+
* Re-emit the last-known state after the bridge transport reconnects.
|
|
58
|
+
* No-op when no state has been observed yet (cold start before the
|
|
59
|
+
* first probe completes). When `last` is non-null, treats reconnect
|
|
60
|
+
* as a "deliver-once" opportunity regardless of whether the value
|
|
61
|
+
* has changed — the cluster might have lost the prior message.
|
|
62
|
+
*/
|
|
63
|
+
private flushOnReconnect;
|
|
64
|
+
private send;
|
|
65
|
+
/** Tear down both subscriptions. Safe to call multiple times. */
|
|
66
|
+
dispose(): void;
|
|
67
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HealthReporter — bridges MCPHealthMonitor state changes onto the
|
|
3
|
+
* Bridge ACP transport as `device/health` notifications, and re-emits
|
|
4
|
+
* the last-known state on every transport reconnect.
|
|
5
|
+
*
|
|
6
|
+
* Why the reconnect re-emit?
|
|
7
|
+
* The bridge-client SDK auto-reconnects on transport loss, but its
|
|
8
|
+
* `send()` quietly drops messages while disconnected (`.catch(()
|
|
9
|
+
* => undefined)` in the inbound notifier). Without a reconnect-time
|
|
10
|
+
* replay, a transition that fires while the WebSocket is down is
|
|
11
|
+
* lost forever — and the cluster's view of `connectivity_reason` is
|
|
12
|
+
* stuck on the prior value. The "connected" event handler here
|
|
13
|
+
* makes every reconnect an opportunity to re-sync server-side state.
|
|
14
|
+
*
|
|
15
|
+
* trace_id:
|
|
16
|
+
* Every emission generates a fresh UUID. Logged here, propagated
|
|
17
|
+
* verbatim by Bridge → BridgeHub → Cluster (via the new
|
|
18
|
+
* `connectivity_reason` field carrying it through), and surfaced in
|
|
19
|
+
* each hop's structured logs. On-call can grep one ID and see the
|
|
20
|
+
* whole chain.
|
|
21
|
+
*
|
|
22
|
+
* Lifecycle:
|
|
23
|
+
* `dispose()` removes both the monitor `change` listener and the
|
|
24
|
+
* transport `connected` listener — the IBridgeTransport now exposes
|
|
25
|
+
* `off()` precisely so this reporter can clean up symmetrically.
|
|
26
|
+
*/
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
28
|
+
export class HealthReporter {
|
|
29
|
+
monitor;
|
|
30
|
+
transport;
|
|
31
|
+
logger;
|
|
32
|
+
generateTraceId;
|
|
33
|
+
onChange;
|
|
34
|
+
onConnected;
|
|
35
|
+
/** Last reported state. Null until the first `change` event fires. */
|
|
36
|
+
last = null;
|
|
37
|
+
disposed = false;
|
|
38
|
+
constructor(opts) {
|
|
39
|
+
this.monitor = opts.monitor;
|
|
40
|
+
this.transport = opts.transport;
|
|
41
|
+
this.logger = opts.logger.child({ component: "health-reporter" });
|
|
42
|
+
this.generateTraceId = opts.generateTraceId ?? randomUUID;
|
|
43
|
+
this.onChange = (s) => this.maybeReport(s);
|
|
44
|
+
this.onConnected = () => this.flushOnReconnect();
|
|
45
|
+
this.monitor.on("change", this.onChange);
|
|
46
|
+
this.transport.on("connected", this.onConnected);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Edge-triggered emission: only sends when the new state differs from
|
|
50
|
+
* the last reported one. Suppresses redundant traffic from the
|
|
51
|
+
* underlying monitor's idempotent re-fires (the monitor itself does
|
|
52
|
+
* the same suppression, but the double-check here makes the reporter
|
|
53
|
+
* safe to drive from any other source as well).
|
|
54
|
+
*/
|
|
55
|
+
maybeReport(s) {
|
|
56
|
+
if (this.disposed)
|
|
57
|
+
return;
|
|
58
|
+
if (this.last && equalState(this.last, s))
|
|
59
|
+
return;
|
|
60
|
+
this.last = s;
|
|
61
|
+
this.send(s, "edge");
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Re-emit the last-known state after the bridge transport reconnects.
|
|
65
|
+
* No-op when no state has been observed yet (cold start before the
|
|
66
|
+
* first probe completes). When `last` is non-null, treats reconnect
|
|
67
|
+
* as a "deliver-once" opportunity regardless of whether the value
|
|
68
|
+
* has changed — the cluster might have lost the prior message.
|
|
69
|
+
*/
|
|
70
|
+
flushOnReconnect() {
|
|
71
|
+
if (this.disposed)
|
|
72
|
+
return;
|
|
73
|
+
if (!this.last)
|
|
74
|
+
return;
|
|
75
|
+
this.send(this.last, "reconnect");
|
|
76
|
+
}
|
|
77
|
+
send(s, cause) {
|
|
78
|
+
const traceId = this.generateTraceId();
|
|
79
|
+
const params = s.healthy
|
|
80
|
+
? { healthy: true, trace_id: traceId }
|
|
81
|
+
: { healthy: false, reason: s.reason, trace_id: traceId };
|
|
82
|
+
this.logger.info({
|
|
83
|
+
trace_id: traceId,
|
|
84
|
+
cause,
|
|
85
|
+
healthy: s.healthy,
|
|
86
|
+
reason: s.healthy ? undefined : s.reason,
|
|
87
|
+
}, "device health reported");
|
|
88
|
+
void this.transport
|
|
89
|
+
.send(JSON.stringify({
|
|
90
|
+
jsonrpc: "2.0",
|
|
91
|
+
method: "device/health",
|
|
92
|
+
params,
|
|
93
|
+
}))
|
|
94
|
+
.catch((err) => {
|
|
95
|
+
// Transport dropped the send (typically: disconnected). We
|
|
96
|
+
// intentionally do not retry inline — the next monitor edge or
|
|
97
|
+
// the next "connected" event will pick it up. Log at debug
|
|
98
|
+
// because this is the expected failure mode during reconnect
|
|
99
|
+
// windows, not an error to alert on.
|
|
100
|
+
this.logger.debug({
|
|
101
|
+
trace_id: traceId,
|
|
102
|
+
cause,
|
|
103
|
+
err: err instanceof Error ? err.message : String(err),
|
|
104
|
+
}, "device/health send rejected (will retry on next edge / reconnect)");
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Tear down both subscriptions. Safe to call multiple times. */
|
|
108
|
+
dispose() {
|
|
109
|
+
if (this.disposed)
|
|
110
|
+
return;
|
|
111
|
+
this.disposed = true;
|
|
112
|
+
this.monitor.off("change", this.onChange);
|
|
113
|
+
this.transport.off("connected", this.onConnected);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function equalState(a, b) {
|
|
117
|
+
if (a.healthy !== b.healthy)
|
|
118
|
+
return false;
|
|
119
|
+
if (!a.healthy && !b.healthy)
|
|
120
|
+
return a.reason === b.reason;
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=health-reporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-reporter.js","sourceRoot":"","sources":["../../src/health/health-reporter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { MCPHealthMonitor } from "./mcp-health-monitor.js";
|
|
2
|
+
export type { HealthState, MCPHealthMonitorOptions } from "./mcp-health-monitor.js";
|
|
3
|
+
export { HealthReporter } from "./health-reporter.js";
|
|
4
|
+
export type { HealthReporterOptions } from "./health-reporter.js";
|
|
5
|
+
export { AdbTrackDevices } from "./adb-track-devices.js";
|
|
6
|
+
export type { AdbTrackDevicesOptions, AdbHealthState } from "./adb-track-devices.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/health/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC"}
|