@aiassesstech/mighty-mark 0.5.7 → 0.5.9

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.
@@ -0,0 +1,759 @@
1
+ /**
2
+ * Fleet-bus transport health checks — 8 checks in the 'fleet' category.
3
+ *
4
+ * These checks validate Layers 1-2 of the fleet-bus stack (Gateway + Transport),
5
+ * complementing the existing checks in fleet-health.ts that cover Layer 3 (Bus Logic).
6
+ *
7
+ * Audit-based (filesystem reads only, no gateway access needed):
8
+ * #40 fleet-transport-delivery At least 1 outcome=delivered/success in 24h
9
+ * #41 fleet-transport-error-rate Error rate < 30% (24h), trend < 10% (7d)
10
+ * #40a fleet-agent:{name} Per-agent delivery status (one check per agent)
11
+ *
12
+ * Config/version (filesystem reads, optional shell exec):
13
+ * #42 fleet-config-validation No config warnings in logs + no legacy symlinks
14
+ * #43 fleet-gateway-version-compat Installed OpenClaw ≥ fleet-bus openclaw.minVersion
15
+ * #44 fleet-global-package-version Global fleet-bus version ≥ expected minimum
16
+ * #45 fleet-callgateway-resolution gateway/call.js exists at expected paths
17
+ * #46 fleet-acp-dispatch-status ACP dispatch explicitly disabled
18
+ *
19
+ * Live (opt-in, requires gateway process):
20
+ * #47 fleet-live-ping Self-ping via transportSend, verify delivery
21
+ *
22
+ * Spec: SPEC-FLEET-BUS-HEALTH-CHECK-BATTERY-1.1
23
+ */
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { exec, now, resolveOpenClawHome, getConfigPath } from './check-context.js';
27
+ const EXPECTED_AGENTS = ['grillo', 'jessie', 'noah', 'nole', 'mighty-mark', 'sam'];
28
+ const DEFAULT_ERROR_RATE_WARN = 0.1;
29
+ const DEFAULT_ERROR_RATE_FAIL = 0.3;
30
+ function getFleetBusDir(ctx) {
31
+ return ctx?.fleetBusDir ?? path.join(resolveOpenClawHome(), 'workspace', 'fleet-bus');
32
+ }
33
+ function readAuditEventsByAgent(auditDir) {
34
+ const byAgent = new Map();
35
+ for (const agent of EXPECTED_AGENTS) {
36
+ const jsonlPath = path.join(auditDir, `${agent}.audit.jsonl`);
37
+ if (!fs.existsSync(jsonlPath))
38
+ continue;
39
+ try {
40
+ const content = fs.readFileSync(jsonlPath, 'utf-8').trim();
41
+ if (!content)
42
+ continue;
43
+ const events = [];
44
+ for (const line of content.split('\n')) {
45
+ if (!line.trim())
46
+ continue;
47
+ try {
48
+ events.push(JSON.parse(line));
49
+ }
50
+ catch {
51
+ // skip malformed lines
52
+ }
53
+ }
54
+ if (events.length > 0) {
55
+ byAgent.set(agent, events);
56
+ }
57
+ }
58
+ catch {
59
+ // skip unreadable files
60
+ }
61
+ }
62
+ return byAgent;
63
+ }
64
+ function readAuditEvents(auditDir) {
65
+ const all = [];
66
+ for (const events of readAuditEventsByAgent(auditDir).values()) {
67
+ all.push(...events);
68
+ }
69
+ return all;
70
+ }
71
+ function isSuccessOutcome(event) {
72
+ return event.outcome === 'delivered' || event.outcome === 'success';
73
+ }
74
+ function filterByAge(events, maxAgeMs) {
75
+ const cutoff = Date.now() - maxAgeMs;
76
+ return events.filter((e) => {
77
+ if (!e.timestamp)
78
+ return false;
79
+ return new Date(e.timestamp).getTime() >= cutoff;
80
+ });
81
+ }
82
+ // ── #40 Fleet Transport Delivery ────────────────────────────────
83
+ /**
84
+ * #40 — At least one message delivered in the last 24 hours.
85
+ *
86
+ * Accepts both `outcome=delivered` (current) and `outcome=success`
87
+ * (future guaranteed-delivery). See BB Amendment #1.
88
+ */
89
+ export async function checkFleetTransportDelivery(ctx) {
90
+ const start = Date.now();
91
+ const auditDir = path.join(getFleetBusDir(ctx), 'audit');
92
+ if (!fs.existsSync(auditDir)) {
93
+ return {
94
+ name: 'Fleet transport delivery',
95
+ category: 'fleet',
96
+ status: 'warn',
97
+ message: 'No audit directory — bus has never sent a message',
98
+ timestamp: now(),
99
+ durationMs: Date.now() - start,
100
+ };
101
+ }
102
+ const allEvents = readAuditEvents(auditDir);
103
+ const recent = filterByAge(allEvents, 24 * 60 * 60 * 1000);
104
+ if (recent.length === 0) {
105
+ return {
106
+ name: 'Fleet transport delivery',
107
+ category: 'fleet',
108
+ status: 'warn',
109
+ message: 'No fleet messages in last 24h (bus idle)',
110
+ timestamp: now(),
111
+ durationMs: Date.now() - start,
112
+ };
113
+ }
114
+ const delivered = recent.filter(isSuccessOutcome).length;
115
+ if (delivered === 0) {
116
+ return {
117
+ name: 'Fleet transport delivery',
118
+ category: 'fleet',
119
+ status: 'fail',
120
+ message: `0 delivered / ${recent.length} total — 100% failure rate (transport broken)`,
121
+ timestamp: now(),
122
+ durationMs: Date.now() - start,
123
+ };
124
+ }
125
+ return {
126
+ name: 'Fleet transport delivery',
127
+ category: 'fleet',
128
+ status: 'pass',
129
+ message: `${delivered} delivered / ${recent.length} total in last 24h`,
130
+ timestamp: now(),
131
+ durationMs: Date.now() - start,
132
+ };
133
+ }
134
+ // ── #41 Fleet Transport Error Rate ──────────────────────────────
135
+ /**
136
+ * #41 — Monitor error rate with dual-window approach.
137
+ *
138
+ * Primary window (24h) for FAIL threshold, secondary (7d) for trend WARN.
139
+ * Thresholds: 10% WARN / 30% FAIL (BB Amendment #4 & #7).
140
+ */
141
+ export async function checkFleetTransportErrorRate(ctx) {
142
+ const start = Date.now();
143
+ const auditDir = path.join(getFleetBusDir(ctx), 'audit');
144
+ const warnThreshold = ctx?.fleetErrorRateWarn ?? DEFAULT_ERROR_RATE_WARN;
145
+ const failThreshold = ctx?.fleetErrorRateFail ?? DEFAULT_ERROR_RATE_FAIL;
146
+ if (!fs.existsSync(auditDir)) {
147
+ return {
148
+ name: 'Fleet transport error rate',
149
+ category: 'fleet',
150
+ status: 'pass',
151
+ message: 'No audit trail — no errors to report',
152
+ timestamp: now(),
153
+ durationMs: Date.now() - start,
154
+ };
155
+ }
156
+ const allEvents = readAuditEvents(auditDir);
157
+ const events24h = filterByAge(allEvents, 24 * 60 * 60 * 1000);
158
+ const events7d = filterByAge(allEvents, 7 * 24 * 60 * 60 * 1000);
159
+ if (events24h.length === 0 && events7d.length === 0) {
160
+ return {
161
+ name: 'Fleet transport error rate',
162
+ category: 'fleet',
163
+ status: 'pass',
164
+ message: 'No audit events in last 7 days',
165
+ timestamp: now(),
166
+ durationMs: Date.now() - start,
167
+ };
168
+ }
169
+ if (events24h.length > 0) {
170
+ const errors24h = events24h.filter((e) => e.outcome === 'error').length;
171
+ const rate24h = errors24h / events24h.length;
172
+ if (rate24h >= failThreshold) {
173
+ return {
174
+ name: 'Fleet transport error rate',
175
+ category: 'fleet',
176
+ status: 'fail',
177
+ message: `Error rate ${pct(rate24h)} (${errors24h}/${events24h.length} events failed, 24h) — transport broken`,
178
+ timestamp: now(),
179
+ durationMs: Date.now() - start,
180
+ };
181
+ }
182
+ if (rate24h >= warnThreshold) {
183
+ return {
184
+ name: 'Fleet transport error rate',
185
+ category: 'fleet',
186
+ status: 'warn',
187
+ message: `Error rate ${pct(rate24h)} (${errors24h}/${events24h.length} events failed, 24h) — investigate transport`,
188
+ timestamp: now(),
189
+ durationMs: Date.now() - start,
190
+ };
191
+ }
192
+ }
193
+ if (events7d.length > 0) {
194
+ const errors7d = events7d.filter((e) => e.outcome === 'error').length;
195
+ const rate7d = errors7d / events7d.length;
196
+ if (rate7d >= warnThreshold) {
197
+ return {
198
+ name: 'Fleet transport error rate',
199
+ category: 'fleet',
200
+ status: 'warn',
201
+ message: `24h clean but 7d trend: ${pct(rate7d)} (${errors7d}/${events7d.length} events) — intermittent failures`,
202
+ timestamp: now(),
203
+ durationMs: Date.now() - start,
204
+ };
205
+ }
206
+ }
207
+ const count = events24h.length > 0 ? events24h.length : events7d.length;
208
+ const delivered = events24h.length > 0
209
+ ? events24h.filter(isSuccessOutcome).length
210
+ : events7d.filter(isSuccessOutcome).length;
211
+ return {
212
+ name: 'Fleet transport error rate',
213
+ category: 'fleet',
214
+ status: 'pass',
215
+ message: `Error rate 0% — ${delivered}/${count} events delivered (${events24h.length > 0 ? '24h' : '7d'})`,
216
+ timestamp: now(),
217
+ durationMs: Date.now() - start,
218
+ };
219
+ }
220
+ // ── #40a Per-Agent Transport Delivery ───────────────────────────
221
+ /**
222
+ * #40a — Per-agent delivery status (one result per agent).
223
+ *
224
+ * Returns one CheckResult per agent that has an audit file.
225
+ * Agents with 0 deliveries and >0 events get FAIL; agents with
226
+ * no events get WARN (idle); agents with at least 1 delivery get PASS.
227
+ */
228
+ export async function checkPerAgentDelivery(ctx) {
229
+ const start = Date.now();
230
+ const auditDir = path.join(getFleetBusDir(ctx), 'audit');
231
+ const results = [];
232
+ if (!fs.existsSync(auditDir)) {
233
+ return results;
234
+ }
235
+ const byAgent = readAuditEventsByAgent(auditDir);
236
+ for (const agent of EXPECTED_AGENTS) {
237
+ const agentEvents = byAgent.get(agent);
238
+ if (!agentEvents || agentEvents.length === 0) {
239
+ results.push({
240
+ name: `Fleet agent: ${agent}`,
241
+ category: 'fleet',
242
+ status: 'warn',
243
+ message: 'No audit events (idle)',
244
+ timestamp: now(),
245
+ durationMs: Date.now() - start,
246
+ });
247
+ continue;
248
+ }
249
+ const recent = filterByAge(agentEvents, 24 * 60 * 60 * 1000);
250
+ if (recent.length === 0) {
251
+ results.push({
252
+ name: `Fleet agent: ${agent}`,
253
+ category: 'fleet',
254
+ status: 'warn',
255
+ message: `${agentEvents.length} total events, none in last 24h`,
256
+ timestamp: now(),
257
+ durationMs: Date.now() - start,
258
+ });
259
+ continue;
260
+ }
261
+ const delivered = recent.filter(isSuccessOutcome).length;
262
+ const errors = recent.filter((e) => e.outcome === 'error').length;
263
+ if (delivered === 0 && errors > 0) {
264
+ results.push({
265
+ name: `Fleet agent: ${agent}`,
266
+ category: 'fleet',
267
+ status: 'fail',
268
+ message: `0/${recent.length} delivered (24h) — transport broken for ${agent}`,
269
+ timestamp: now(),
270
+ durationMs: Date.now() - start,
271
+ });
272
+ continue;
273
+ }
274
+ const rate = recent.length > 0 ? errors / recent.length : 0;
275
+ const warnThreshold = ctx?.fleetErrorRateWarn ?? DEFAULT_ERROR_RATE_WARN;
276
+ if (rate >= warnThreshold) {
277
+ results.push({
278
+ name: `Fleet agent: ${agent}`,
279
+ category: 'fleet',
280
+ status: 'warn',
281
+ message: `${delivered}/${recent.length} delivered, ${pct(rate)} error rate (24h)`,
282
+ timestamp: now(),
283
+ durationMs: Date.now() - start,
284
+ });
285
+ continue;
286
+ }
287
+ results.push({
288
+ name: `Fleet agent: ${agent}`,
289
+ category: 'fleet',
290
+ status: 'pass',
291
+ message: `${delivered}/${recent.length} delivered (24h)`,
292
+ timestamp: now(),
293
+ durationMs: Date.now() - start,
294
+ });
295
+ }
296
+ return results;
297
+ }
298
+ // ── #42 Fleet Config & Symlink Validation ───────────────────────
299
+ /**
300
+ * #42 — Detect config validation warnings + legacy symlinks.
301
+ *
302
+ * Tiered log source: health endpoint → log file → journalctl fallback.
303
+ * Also detects the clawdbot.json symlink (BB Amendment #2 & #8).
304
+ */
305
+ export async function checkFleetConfigValidation(ctx) {
306
+ const start = Date.now();
307
+ const issues = [];
308
+ // Sub-check 1: Config validation warnings in gateway logs
309
+ let logContent = '';
310
+ try {
311
+ const logPaths = [
312
+ '/var/log/openclaw/gateway.log',
313
+ path.join(resolveOpenClawHome(), 'logs', 'gateway.log'),
314
+ ];
315
+ for (const logPath of logPaths) {
316
+ if (fs.existsSync(logPath)) {
317
+ logContent = fs.readFileSync(logPath, 'utf-8');
318
+ break;
319
+ }
320
+ }
321
+ if (!logContent) {
322
+ logContent = exec('journalctl -u openclaw-gateway --no-pager --since "1 hour ago" 2>/dev/null || echo ""', ctx);
323
+ }
324
+ }
325
+ catch {
326
+ // No log source available — not a failure, just can't check
327
+ }
328
+ if (logContent) {
329
+ const invalidConfigMatches = logContent.match(/Invalid config at|Unrecognized key/gi);
330
+ if (invalidConfigMatches && invalidConfigMatches.length > 0) {
331
+ issues.push(`${invalidConfigMatches.length} config validation warning(s) — callGateway may fail`);
332
+ }
333
+ }
334
+ // Sub-check 2: Legacy symlink detection (BB Amendment #8)
335
+ const symlinkPaths = [
336
+ '/root/.clawdbot/clawdbot.json',
337
+ path.join(process.env.HOME || '', '.clawdbot', 'clawdbot.json'),
338
+ ];
339
+ for (const symlinkPath of symlinkPaths) {
340
+ try {
341
+ if (fs.existsSync(symlinkPath) && fs.lstatSync(symlinkPath).isSymbolicLink()) {
342
+ issues.push(`Legacy symlink ${symlinkPath} detected — remove and set OPENCLAW_HOME`);
343
+ break;
344
+ }
345
+ }
346
+ catch {
347
+ // Path doesn't exist or isn't accessible
348
+ }
349
+ }
350
+ if (issues.length > 0) {
351
+ return {
352
+ name: 'Fleet config validation',
353
+ category: 'fleet',
354
+ status: 'warn',
355
+ message: issues.join('; '),
356
+ timestamp: now(),
357
+ durationMs: Date.now() - start,
358
+ };
359
+ }
360
+ return {
361
+ name: 'Fleet config validation',
362
+ category: 'fleet',
363
+ status: 'pass',
364
+ message: 'No config warnings; no legacy symlinks',
365
+ timestamp: now(),
366
+ durationMs: Date.now() - start,
367
+ };
368
+ }
369
+ // ── #43 Fleet Gateway Version Compatibility ─────────────────────
370
+ /**
371
+ * #43 — Verify OpenClaw version meets fleet-bus minimum.
372
+ *
373
+ * Reads `openclaw.minVersion` from fleet-bus's package.json (BB Amendment #3).
374
+ */
375
+ export async function checkFleetGatewayVersionCompat(ctx) {
376
+ const start = Date.now();
377
+ let installedVersion = 'unknown';
378
+ try {
379
+ const raw = exec('openclaw --version 2>/dev/null || echo "unknown"', ctx);
380
+ const match = raw.match(/(\d{4}\.\d+\.\d+)/);
381
+ installedVersion = match ? match[1] : raw.trim();
382
+ }
383
+ catch {
384
+ return {
385
+ name: 'Fleet gateway version compat',
386
+ category: 'fleet',
387
+ status: 'warn',
388
+ message: 'Could not determine OpenClaw version',
389
+ timestamp: now(),
390
+ durationMs: Date.now() - start,
391
+ };
392
+ }
393
+ const packageJsonPath = ctx?.fleetBusPackageJsonPath ?? findFleetBusPackageJson();
394
+ if (!packageJsonPath) {
395
+ return {
396
+ name: 'Fleet gateway version compat',
397
+ category: 'fleet',
398
+ status: 'warn',
399
+ message: 'fleet-bus package.json not found — cannot verify compatibility',
400
+ timestamp: now(),
401
+ durationMs: Date.now() - start,
402
+ };
403
+ }
404
+ try {
405
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
406
+ const minVersion = pkg?.openclaw?.minVersion;
407
+ if (!minVersion) {
408
+ return {
409
+ name: 'Fleet gateway version compat',
410
+ category: 'fleet',
411
+ status: 'warn',
412
+ message: `fleet-bus ${pkg.version ?? '?'} missing openclaw.minVersion — cannot verify compatibility`,
413
+ timestamp: now(),
414
+ durationMs: Date.now() - start,
415
+ };
416
+ }
417
+ if (installedVersion === 'unknown') {
418
+ return {
419
+ name: 'Fleet gateway version compat',
420
+ category: 'fleet',
421
+ status: 'warn',
422
+ message: `Requires OpenClaw ≥ ${minVersion} but could not determine installed version`,
423
+ timestamp: now(),
424
+ durationMs: Date.now() - start,
425
+ };
426
+ }
427
+ if (compareVersions(installedVersion, minVersion) < 0) {
428
+ return {
429
+ name: 'Fleet gateway version compat',
430
+ category: 'fleet',
431
+ status: 'fail',
432
+ message: `fleet-bus ${pkg.version} requires OpenClaw ≥ ${minVersion} but found ${installedVersion} — upgrade OpenClaw`,
433
+ timestamp: now(),
434
+ durationMs: Date.now() - start,
435
+ };
436
+ }
437
+ return {
438
+ name: 'Fleet gateway version compat',
439
+ category: 'fleet',
440
+ status: 'pass',
441
+ message: `fleet-bus ${pkg.version} requires ≥ ${minVersion}, found ${installedVersion} — compatible`,
442
+ timestamp: now(),
443
+ durationMs: Date.now() - start,
444
+ };
445
+ }
446
+ catch {
447
+ return {
448
+ name: 'Fleet gateway version compat',
449
+ category: 'fleet',
450
+ status: 'warn',
451
+ message: 'Could not read fleet-bus package.json',
452
+ timestamp: now(),
453
+ durationMs: Date.now() - start,
454
+ };
455
+ }
456
+ }
457
+ // ── #44 Fleet Global Package Version ────────────────────────────
458
+ /**
459
+ * #44 — Verify globally installed fleet-bus is at expected minimum.
460
+ */
461
+ export async function checkFleetGlobalPackageVersion(ctx) {
462
+ const start = Date.now();
463
+ const packageJsonPath = ctx?.fleetBusGlobalPackageJsonPath ?? findFleetBusPackageJson();
464
+ if (!packageJsonPath) {
465
+ return {
466
+ name: 'Fleet global package version',
467
+ category: 'fleet',
468
+ status: 'warn',
469
+ message: 'fleet-bus not found at global install path',
470
+ timestamp: now(),
471
+ durationMs: Date.now() - start,
472
+ };
473
+ }
474
+ try {
475
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
476
+ const version = pkg.version;
477
+ const minVersion = pkg?.openclaw?.minVersion;
478
+ const expectedMin = '0.1.8';
479
+ if (compareVersions(version, expectedMin) < 0) {
480
+ return {
481
+ name: 'Fleet global package version',
482
+ category: 'fleet',
483
+ status: 'warn',
484
+ message: `Global fleet-bus ${version} — run: cd /root && npm install @aiassesstech/fleet-bus@latest`,
485
+ timestamp: now(),
486
+ durationMs: Date.now() - start,
487
+ };
488
+ }
489
+ return {
490
+ name: 'Fleet global package version',
491
+ category: 'fleet',
492
+ status: 'pass',
493
+ message: `Global fleet-bus ${version}${minVersion ? ` (requires OpenClaw ≥ ${minVersion})` : ''}`,
494
+ timestamp: now(),
495
+ durationMs: Date.now() - start,
496
+ };
497
+ }
498
+ catch {
499
+ return {
500
+ name: 'Fleet global package version',
501
+ category: 'fleet',
502
+ status: 'warn',
503
+ message: 'Could not read fleet-bus package.json',
504
+ timestamp: now(),
505
+ durationMs: Date.now() - start,
506
+ };
507
+ }
508
+ }
509
+ // ── #45 Fleet callGateway Resolution ────────────────────────────
510
+ /**
511
+ * #45 — Verify that gateway/call.js exists at known paths.
512
+ *
513
+ * This check runs in CLI mode (file existence) since the full resolution
514
+ * strategy requires the gateway process context.
515
+ */
516
+ export async function checkFleetCallGatewayResolution(ctx) {
517
+ const start = Date.now();
518
+ const searchPaths = [
519
+ '/usr/lib/node_modules/openclaw/dist/gateway/call.js',
520
+ '/usr/local/lib/node_modules/openclaw/dist/gateway/call.js',
521
+ path.join(process.env.HOME || '', 'node_modules', 'openclaw', 'dist', 'gateway', 'call.js'),
522
+ path.join(process.env.HOME || '', '.openclaw', 'node_modules', 'openclaw', 'dist', 'gateway', 'call.js'),
523
+ ];
524
+ for (const searchPath of searchPaths) {
525
+ if (fs.existsSync(searchPath)) {
526
+ return {
527
+ name: 'Fleet callGateway resolution',
528
+ category: 'fleet',
529
+ status: 'pass',
530
+ message: `gateway/call.js found at ${searchPath}`,
531
+ timestamp: now(),
532
+ durationMs: Date.now() - start,
533
+ };
534
+ }
535
+ }
536
+ try {
537
+ const npmRoot = exec('npm root -g 2>/dev/null', ctx).trim();
538
+ const npmPath = path.join(npmRoot, 'openclaw', 'dist', 'gateway', 'call.js');
539
+ if (fs.existsSync(npmPath)) {
540
+ return {
541
+ name: 'Fleet callGateway resolution',
542
+ category: 'fleet',
543
+ status: 'pass',
544
+ message: `gateway/call.js found at ${npmPath}`,
545
+ timestamp: now(),
546
+ durationMs: Date.now() - start,
547
+ };
548
+ }
549
+ }
550
+ catch {
551
+ // npm root failed
552
+ }
553
+ return {
554
+ name: 'Fleet callGateway resolution',
555
+ category: 'fleet',
556
+ status: 'fail',
557
+ message: 'gateway/call.js not found — fleet-bus transport will be disabled',
558
+ timestamp: now(),
559
+ durationMs: Date.now() - start,
560
+ };
561
+ }
562
+ // ── #46 Fleet ACP Dispatch Status ───────────────────────────────
563
+ /**
564
+ * #46 — Verify ACP dispatch is explicitly disabled.
565
+ */
566
+ export async function checkFleetAcpDispatchStatus(ctx) {
567
+ const start = Date.now();
568
+ const configPath = getConfigPath(ctx);
569
+ if (!fs.existsSync(configPath)) {
570
+ return {
571
+ name: 'Fleet ACP dispatch',
572
+ category: 'fleet',
573
+ status: 'warn',
574
+ message: `Config not found: ${configPath} — cannot verify ACP dispatch`,
575
+ timestamp: now(),
576
+ durationMs: Date.now() - start,
577
+ };
578
+ }
579
+ try {
580
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
581
+ const dispatchEnabled = config?.acp?.dispatch?.enabled;
582
+ if (dispatchEnabled === false) {
583
+ return {
584
+ name: 'Fleet ACP dispatch',
585
+ category: 'fleet',
586
+ status: 'pass',
587
+ message: 'ACP dispatch disabled',
588
+ timestamp: now(),
589
+ durationMs: Date.now() - start,
590
+ };
591
+ }
592
+ return {
593
+ name: 'Fleet ACP dispatch',
594
+ category: 'fleet',
595
+ status: 'warn',
596
+ message: `ACP dispatch ${dispatchEnabled === true ? 'enabled' : 'not set'} — add acp.dispatch.enabled=false to openclaw.json`,
597
+ timestamp: now(),
598
+ durationMs: Date.now() - start,
599
+ };
600
+ }
601
+ catch {
602
+ return {
603
+ name: 'Fleet ACP dispatch',
604
+ category: 'fleet',
605
+ status: 'warn',
606
+ message: 'Could not parse config — cannot verify ACP dispatch',
607
+ timestamp: now(),
608
+ durationMs: Date.now() - start,
609
+ };
610
+ }
611
+ }
612
+ // ── #47 Fleet Live Ping (Opt-In) ────────────────────────────────
613
+ /**
614
+ * #47 — Send a real fleet/ping through the transport stack.
615
+ *
616
+ * Prefers self-ping (loopback); falls back to agent with most recent lastSeen.
617
+ * Only runs when ctx.fleetLivePing is true (BB Answer Q1).
618
+ */
619
+ export async function checkFleetLivePing(ctx) {
620
+ const start = Date.now();
621
+ // This check requires fleet-bus to be importable
622
+ let fb;
623
+ try {
624
+ fb = await import('@aiassesstech/fleet-bus');
625
+ }
626
+ catch {
627
+ return {
628
+ name: 'Fleet live ping',
629
+ category: 'fleet',
630
+ status: 'warn',
631
+ message: 'fleet-bus not installed — cannot perform live ping',
632
+ timestamp: now(),
633
+ durationMs: Date.now() - start,
634
+ };
635
+ }
636
+ const { createFleetMessage, transportSend } = fb;
637
+ if (!transportSend) {
638
+ return {
639
+ name: 'Fleet live ping',
640
+ category: 'fleet',
641
+ status: 'warn',
642
+ message: 'transportSend not available — gateway context required',
643
+ timestamp: now(),
644
+ durationMs: Date.now() - start,
645
+ };
646
+ }
647
+ const target = selectPingTarget(ctx);
648
+ try {
649
+ const msg = createFleetMessage('mighty-mark', target, 'fleet/ping', { probe: true });
650
+ const pingStart = Date.now();
651
+ await transportSend(msg);
652
+ const latency = Date.now() - pingStart;
653
+ return {
654
+ name: 'Fleet live ping',
655
+ category: 'fleet',
656
+ status: 'pass',
657
+ message: target === 'mighty-mark'
658
+ ? `fleet/ping self-loopback delivered in ${latency}ms`
659
+ : `fleet/ping delivered to ${target} in ${latency}ms`,
660
+ timestamp: now(),
661
+ durationMs: Date.now() - start,
662
+ };
663
+ }
664
+ catch (err) {
665
+ const detail = err instanceof Error ? err.message : String(err);
666
+ return {
667
+ name: 'Fleet live ping',
668
+ category: 'fleet',
669
+ status: 'fail',
670
+ message: `fleet/ping failed: ${detail}`,
671
+ timestamp: now(),
672
+ durationMs: Date.now() - start,
673
+ };
674
+ }
675
+ }
676
+ /**
677
+ * Select the best ping target (BB Amendment #5).
678
+ * Prefers self-ping. Falls back to agent with most recent lastSeen.
679
+ */
680
+ function selectPingTarget(ctx) {
681
+ const cardsDir = path.join(getFleetBusDir(ctx), 'cards');
682
+ if (!fs.existsSync(cardsDir))
683
+ return 'mighty-mark';
684
+ let mostRecent = 'mighty-mark';
685
+ let mostRecentTime = 0;
686
+ for (const agent of EXPECTED_AGENTS) {
687
+ if (agent === 'mighty-mark')
688
+ continue;
689
+ const cardPath = path.join(cardsDir, `${agent}.json`);
690
+ try {
691
+ if (!fs.existsSync(cardPath))
692
+ continue;
693
+ const card = JSON.parse(fs.readFileSync(cardPath, 'utf-8'));
694
+ const lastSeen = new Date(card.lastSeen ?? card.registeredAt).getTime();
695
+ if (lastSeen > mostRecentTime) {
696
+ mostRecentTime = lastSeen;
697
+ mostRecent = agent;
698
+ }
699
+ }
700
+ catch {
701
+ // skip unreadable cards
702
+ }
703
+ }
704
+ return mostRecent;
705
+ }
706
+ // ── Aggregate runner ────────────────────────────────────────────
707
+ /** Run all fleet transport checks. Check #47 only runs when ctx.fleetLivePing is true. */
708
+ export async function runFleetTransportChecks(ctx) {
709
+ const results = [
710
+ await checkFleetTransportDelivery(ctx),
711
+ await checkFleetTransportErrorRate(ctx),
712
+ ...(await checkPerAgentDelivery(ctx)),
713
+ await checkFleetConfigValidation(ctx),
714
+ await checkFleetGatewayVersionCompat(ctx),
715
+ await checkFleetGlobalPackageVersion(ctx),
716
+ await checkFleetCallGatewayResolution(ctx),
717
+ await checkFleetAcpDispatchStatus(ctx),
718
+ ];
719
+ if (ctx?.fleetLivePing) {
720
+ results.push(await checkFleetLivePing(ctx));
721
+ }
722
+ return results;
723
+ }
724
+ // ── Utilities ───────────────────────────────────────────────────
725
+ function pct(rate) {
726
+ return `${Math.round(rate * 100)}%`;
727
+ }
728
+ /**
729
+ * Compare two version strings (supports both semver "x.y.z" and
730
+ * OpenClaw's "yyyy.m.p" format). Returns -1, 0, or 1.
731
+ */
732
+ export function compareVersions(a, b) {
733
+ const pa = a.split('.').map(Number);
734
+ const pb = b.split('.').map(Number);
735
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
736
+ const na = pa[i] ?? 0;
737
+ const nb = pb[i] ?? 0;
738
+ if (na < nb)
739
+ return -1;
740
+ if (na > nb)
741
+ return 1;
742
+ }
743
+ return 0;
744
+ }
745
+ /** Search well-known paths for fleet-bus package.json. */
746
+ function findFleetBusPackageJson() {
747
+ const candidates = [
748
+ path.join(process.env.HOME || '', 'node_modules', '@aiassesstech', 'fleet-bus', 'package.json'),
749
+ path.join(resolveOpenClawHome(), 'extensions', 'mighty-mark', 'node_modules', '@aiassesstech', 'fleet-bus', 'package.json'),
750
+ '/usr/lib/node_modules/@aiassesstech/fleet-bus/package.json',
751
+ '/usr/local/lib/node_modules/@aiassesstech/fleet-bus/package.json',
752
+ ];
753
+ for (const candidate of candidates) {
754
+ if (fs.existsSync(candidate))
755
+ return candidate;
756
+ }
757
+ return null;
758
+ }
759
+ //# sourceMappingURL=fleet-transport.js.map