@beeos-ai/device-agent 0.9.1 → 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.
Files changed (81) hide show
  1. package/dist/adapters/transport-bridge-ws.d.ts +12 -0
  2. package/dist/adapters/transport-bridge-ws.js +27 -3
  3. package/dist/adapters/transport-bridge-ws.js.map +1 -1
  4. package/dist/agent-runtime/core.d.ts +1 -1
  5. package/dist/agent-runtime/core.js +41 -0
  6. package/dist/agent-runtime/core.js.map +1 -1
  7. package/dist/app/session-manager.d.ts +14 -11
  8. package/dist/app/session-manager.js +8 -9
  9. package/dist/app/session-manager.js.map +1 -1
  10. package/dist/bootstrap.js +140 -11
  11. package/dist/bootstrap.js.map +1 -1
  12. package/dist/chat.js +11 -1
  13. package/dist/chat.js.map +1 -1
  14. package/dist/config/loader.js +3 -0
  15. package/dist/config/loader.js.map +1 -1
  16. package/dist/config/schema.d.ts +3 -0
  17. package/dist/config/schema.js +51 -15
  18. package/dist/config/schema.js.map +1 -1
  19. package/dist/fleet/adapters/process/node-process-manager.d.ts +115 -13
  20. package/dist/fleet/adapters/process/node-process-manager.js +412 -64
  21. package/dist/fleet/adapters/process/node-process-manager.js.map +1 -1
  22. package/dist/fleet/adapters/state/port-allocator.d.ts +12 -0
  23. package/dist/fleet/adapters/state/port-allocator.js +39 -5
  24. package/dist/fleet/adapters/state/port-allocator.js.map +1 -1
  25. package/dist/fleet/domain/events.d.ts +23 -1
  26. package/dist/fleet/domain/fsm.d.ts +13 -1
  27. package/dist/fleet/domain/fsm.js +70 -0
  28. package/dist/fleet/domain/fsm.js.map +1 -1
  29. package/dist/fleet/supervisor.js +30 -0
  30. package/dist/fleet/supervisor.js.map +1 -1
  31. package/dist/health/adb-track-devices.d.ts +103 -0
  32. package/dist/health/adb-track-devices.js +171 -0
  33. package/dist/health/adb-track-devices.js.map +1 -0
  34. package/dist/health/health-reporter.d.ts +67 -0
  35. package/dist/health/health-reporter.js +123 -0
  36. package/dist/health/health-reporter.js.map +1 -0
  37. package/dist/health/index.d.ts +6 -0
  38. package/dist/health/index.js +4 -0
  39. package/dist/health/index.js.map +1 -0
  40. package/dist/health/mcp-health-monitor.d.ts +190 -0
  41. package/dist/health/mcp-health-monitor.js +267 -0
  42. package/dist/health/mcp-health-monitor.js.map +1 -0
  43. package/dist/inbound/cli/main.js +8 -1
  44. package/dist/inbound/cli/main.js.map +1 -1
  45. package/dist/inbound/cli/run-command.js +11 -1
  46. package/dist/inbound/cli/run-command.js.map +1 -1
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/mcp-client/action-mapping.d.ts +45 -0
  50. package/dist/mcp-client/action-mapping.js +224 -0
  51. package/dist/mcp-client/action-mapping.js.map +1 -0
  52. package/dist/mcp-client/autospawn.d.ts +15 -2
  53. package/dist/mcp-client/autospawn.js +82 -4
  54. package/dist/mcp-client/autospawn.js.map +1 -1
  55. package/dist/mcp-client/client.d.ts +134 -38
  56. package/dist/mcp-client/client.js +394 -87
  57. package/dist/mcp-client/client.js.map +1 -1
  58. package/dist/mcp-client/index.d.ts +5 -3
  59. package/dist/mcp-client/index.js +2 -1
  60. package/dist/mcp-client/index.js.map +1 -1
  61. package/dist/mcp-client/inproc.d.ts +2 -2
  62. package/dist/mcp-client/inproc.js.map +1 -1
  63. package/dist/mcp-client/runtime.d.ts +124 -21
  64. package/dist/mcp-client/runtime.js +18 -7
  65. package/dist/mcp-client/runtime.js.map +1 -1
  66. package/dist/model/adapters/autoglm.js +44 -45
  67. package/dist/model/adapters/autoglm.js.map +1 -1
  68. package/dist/plugins/acp/index.d.ts +35 -0
  69. package/dist/plugins/acp/index.js +121 -0
  70. package/dist/plugins/acp/index.js.map +1 -1
  71. package/dist/plugins/message-bus/busy-reply.js +13 -5
  72. package/dist/plugins/message-bus/busy-reply.js.map +1 -1
  73. package/dist/plugins/message-bus/inbound.js +12 -6
  74. package/dist/plugins/message-bus/inbound.js.map +1 -1
  75. package/dist/plugins/message-bus/index.js +14 -2
  76. package/dist/plugins/message-bus/index.js.map +1 -1
  77. package/dist/plugins/message-bus/ms-channel-notifier.d.ts +19 -7
  78. package/dist/plugins/message-bus/ms-channel-notifier.js +33 -8
  79. package/dist/plugins/message-bus/ms-channel-notifier.js.map +1 -1
  80. package/dist/ports/transport.d.ts +11 -0
  81. package/package.json +4 -3
@@ -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,4 @@
1
+ export { MCPHealthMonitor } from "./mcp-health-monitor.js";
2
+ export { HealthReporter } from "./health-reporter.js";
3
+ export { AdbTrackDevices } from "./adb-track-devices.js";
4
+ //# sourceMappingURL=index.js.map
@@ -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"}