@ouro.bot/cli 0.1.0-alpha.531 → 0.1.0-alpha.533
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/changelog.json +16 -0
- package/dist/heart/daemon/cli-exec.js +23 -0
- package/dist/heart/daemon/cli-help.js +2 -2
- package/dist/heart/daemon/cli-parse.js +30 -1
- package/dist/heart/daemon/cli-render.js +28 -1
- package/dist/heart/daemon/daemon-entry.js +33 -1
- package/dist/heart/daemon/daemon.js +14 -57
- package/dist/heart/daemon/health-monitor.js +5 -0
- package/dist/heart/daemon/mcp-canary.js +288 -0
- package/dist/repertoire/mcp-client.js +41 -3
- package/dist/repertoire/mcp-manager.js +60 -3
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.533",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Locks the `ouro.bot` bootstrap wrapper to install the matching `@ouro.bot/cli` package version from its own package metadata instead of resolving `@ouro.bot/cli@latest`, preventing stale npm dist-tags from downgrading or skipping a just-published alpha.",
|
|
8
|
+
"Adds wrapper bootstrap coverage that fails if the wrapper queries `npm view` for `latest` during install, closing the release-smoke gap that caught `ouro.bot@0.1.0-alpha.532` reporting the older CLI."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.532",
|
|
13
|
+
"changes": [
|
|
14
|
+
"MCP clients now classify dead transports, reconnect stale servers before tool calls, retry one transport-level failure, and expose live refresh canaries so closed MCP pipes recover instead of poisoning long-running sessions.",
|
|
15
|
+
"Adds `ouro mcp canary --agent <name>` plus daemon health-monitor wiring, surfacing fresh MCP status failures in `ouro status` and validating daemon/MCP version parity and required sense health.",
|
|
16
|
+
"Explicit `daemon.stop` now writes a last-known down health state instead of creating a misleading unexpected-clean-exit tombstone, daemon background update checks no longer self-restart the live runtime, and `ouro msg` wakes the target agent before IPC delivery.",
|
|
17
|
+
"Release publishing now tags prereleases with their prerelease channel and release smoke verifies exact wrapper versions, keeping npm dist-tags from driving surprise prerelease churn through the daemon updater."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
4
20
|
{
|
|
5
21
|
"version": "0.1.0-alpha.531",
|
|
6
22
|
"changes": [
|
|
@@ -116,6 +116,7 @@ const boot_sync_probe_1 = require("./boot-sync-probe");
|
|
|
116
116
|
const connect_bay_1 = require("./connect-bay");
|
|
117
117
|
const runtime_capability_check_1 = require("../runtime-capability-check");
|
|
118
118
|
const vault_items_1 = require("./vault-items");
|
|
119
|
+
const mcp_canary_1 = require("./mcp-canary");
|
|
119
120
|
// ── ensureDaemonRunning ──
|
|
120
121
|
const DEFAULT_DAEMON_STARTUP_TIMEOUT_MS = 60_000;
|
|
121
122
|
const DEFAULT_DAEMON_STARTUP_POLL_INTERVAL_MS = 500;
|
|
@@ -6516,6 +6517,28 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
6516
6517
|
return message;
|
|
6517
6518
|
}
|
|
6518
6519
|
}
|
|
6520
|
+
if (command.kind === "mcp.canary") {
|
|
6521
|
+
const canarySocketPath = command.socketOverride ?? deps.socketPath;
|
|
6522
|
+
const result = await (0, mcp_canary_1.runMcpStatusCanary)({
|
|
6523
|
+
agent: command.agent,
|
|
6524
|
+
socketPath: canarySocketPath,
|
|
6525
|
+
command: process.execPath,
|
|
6526
|
+
commandArgs: [
|
|
6527
|
+
path.join(__dirname, "ouro-bot-entry.js"),
|
|
6528
|
+
"mcp-serve",
|
|
6529
|
+
"--agent",
|
|
6530
|
+
command.agent,
|
|
6531
|
+
"--socket",
|
|
6532
|
+
canarySocketPath,
|
|
6533
|
+
],
|
|
6534
|
+
requiredSenses: command.requiredSenses ?? [],
|
|
6535
|
+
});
|
|
6536
|
+
if (!result.ok)
|
|
6537
|
+
deps.setExitCode?.(1);
|
|
6538
|
+
const message = command.json ? JSON.stringify(result, null, 2) : (0, mcp_canary_1.formatMcpStatusCanaryResult)(result);
|
|
6539
|
+
deps.writeStdout(message);
|
|
6540
|
+
return message;
|
|
6541
|
+
}
|
|
6519
6542
|
/* v8 ignore start — mcp-serve block binds to process.stdin/stdout; tested via mcp-server unit tests */
|
|
6520
6543
|
// ── mcp-serve: start MCP server in-process on stdin/stdout ──
|
|
6521
6544
|
if (command.kind === "mcp-serve") {
|
|
@@ -253,8 +253,8 @@ exports.COMMAND_REGISTRY = {
|
|
|
253
253
|
category: "System",
|
|
254
254
|
description: "Interact with MCP servers",
|
|
255
255
|
usage: "ouro mcp <subcommand>",
|
|
256
|
-
example: "ouro mcp
|
|
257
|
-
subcommands: ["list", "call"],
|
|
256
|
+
example: "ouro mcp canary --agent ouroboros",
|
|
257
|
+
subcommands: ["list", "call", "canary"],
|
|
258
258
|
},
|
|
259
259
|
"mcp-serve": {
|
|
260
260
|
category: "System",
|
|
@@ -1223,7 +1223,8 @@ function parseConfigCommand(args) {
|
|
|
1223
1223
|
throw new Error(`Usage\n${usage()}`);
|
|
1224
1224
|
}
|
|
1225
1225
|
function parseMcpCommand(args) {
|
|
1226
|
-
const
|
|
1226
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
1227
|
+
const [sub, ...rest] = cleaned;
|
|
1227
1228
|
if (!sub)
|
|
1228
1229
|
throw new Error(`Usage\n${usage()}`);
|
|
1229
1230
|
if (sub === "list")
|
|
@@ -1237,6 +1238,34 @@ function parseMcpCommand(args) {
|
|
|
1237
1238
|
const mcpArgs = argsIdx !== -1 && rest[argsIdx + 1] ? rest[argsIdx + 1] : undefined;
|
|
1238
1239
|
return { kind: "mcp.call", server, tool, ...(mcpArgs ? { args: mcpArgs } : {}) };
|
|
1239
1240
|
}
|
|
1241
|
+
if (sub === "canary") {
|
|
1242
|
+
let socketOverride;
|
|
1243
|
+
let json = false;
|
|
1244
|
+
const requiredSenses = [];
|
|
1245
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1246
|
+
if (rest[i] === "--socket" && rest[i + 1]) {
|
|
1247
|
+
socketOverride = rest[++i];
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (rest[i] === "--require-sense" && rest[i + 1]) {
|
|
1251
|
+
requiredSenses.push(rest[++i]);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
if (rest[i] === "--json") {
|
|
1255
|
+
json = true;
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
if (!agent)
|
|
1260
|
+
throw new Error("mcp canary requires --agent <name>");
|
|
1261
|
+
return {
|
|
1262
|
+
kind: "mcp.canary",
|
|
1263
|
+
agent,
|
|
1264
|
+
...(socketOverride ? { socketOverride } : {}),
|
|
1265
|
+
...(requiredSenses.length > 0 ? { requiredSenses } : {}),
|
|
1266
|
+
...(json ? { json: true } : {}),
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1240
1269
|
throw new Error(`Usage\n${usage()}`);
|
|
1241
1270
|
}
|
|
1242
1271
|
function inferAgentNameFromRemote(remote) {
|
|
@@ -121,6 +121,7 @@ function parseStatusPayload(data) {
|
|
|
121
121
|
const sync = raw.sync;
|
|
122
122
|
const agents = raw.agents;
|
|
123
123
|
const providers = raw.providers;
|
|
124
|
+
const healthChecks = raw.healthChecks;
|
|
124
125
|
if (!overview || typeof overview !== "object" || Array.isArray(overview))
|
|
125
126
|
return null;
|
|
126
127
|
if (!Array.isArray(senses) || !Array.isArray(workers))
|
|
@@ -132,6 +133,8 @@ function parseStatusPayload(data) {
|
|
|
132
133
|
return null;
|
|
133
134
|
if (providers !== undefined && !Array.isArray(providers))
|
|
134
135
|
return null;
|
|
136
|
+
if (healthChecks !== undefined && !Array.isArray(healthChecks))
|
|
137
|
+
return null;
|
|
135
138
|
const parsedOverview = {
|
|
136
139
|
daemon: stringField(overview.daemon) ?? "unknown",
|
|
137
140
|
health: stringField(overview.health) ?? "unknown",
|
|
@@ -281,16 +284,29 @@ function parseStatusPayload(data) {
|
|
|
281
284
|
parsed.detail = detail;
|
|
282
285
|
return parsed;
|
|
283
286
|
});
|
|
287
|
+
const parsedHealthChecks = (healthChecks ?? []).map((entry) => {
|
|
288
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
289
|
+
return null;
|
|
290
|
+
const row = entry;
|
|
291
|
+
const name = stringField(row.name);
|
|
292
|
+
const status = stringField(row.status);
|
|
293
|
+
const message = stringField(row.message);
|
|
294
|
+
if (!name || !status || !message)
|
|
295
|
+
return null;
|
|
296
|
+
return { name, status, message };
|
|
297
|
+
});
|
|
284
298
|
if (parsedSenses.some((row) => row === null) ||
|
|
285
299
|
parsedWorkers.some((row) => row === null) ||
|
|
286
300
|
parsedSync.some((row) => row === null) ||
|
|
287
301
|
parsedAgents.some((row) => row === null) ||
|
|
288
|
-
parsedProviders.some((row) => row === null)
|
|
302
|
+
parsedProviders.some((row) => row === null) ||
|
|
303
|
+
parsedHealthChecks.some((row) => row === null))
|
|
289
304
|
return null;
|
|
290
305
|
return {
|
|
291
306
|
overview: parsedOverview,
|
|
292
307
|
senses: parsedSenses,
|
|
293
308
|
workers: parsedWorkers,
|
|
309
|
+
healthChecks: parsedHealthChecks,
|
|
294
310
|
sync: parsedSync,
|
|
295
311
|
agents: parsedAgents,
|
|
296
312
|
providers: parsedProviders,
|
|
@@ -482,6 +498,16 @@ function formatDaemonStatusOutput(response, fallback) {
|
|
|
482
498
|
}
|
|
483
499
|
lines.push("");
|
|
484
500
|
}
|
|
501
|
+
// ── Health Checks ──
|
|
502
|
+
if (payload.healthChecks.length > 0) {
|
|
503
|
+
lines.push(` ${teal("──")} ${bold("Health Checks")} ${teal("─".repeat(29))}`);
|
|
504
|
+
const nameWidth = Math.max(16, ...payload.healthChecks.map((r) => r.name.length));
|
|
505
|
+
const statusWidth = Math.max(8, ...payload.healthChecks.map((r) => r.status.length));
|
|
506
|
+
for (const row of payload.healthChecks) {
|
|
507
|
+
lines.push(` ${row.name.padEnd(nameWidth)} ${statusDot(row.status)} ${row.status.padEnd(statusWidth)} ${dim(row.message)}`);
|
|
508
|
+
}
|
|
509
|
+
lines.push("");
|
|
510
|
+
}
|
|
485
511
|
// ── Git Sync (per agent) ──
|
|
486
512
|
if (payload.sync.length > 0) {
|
|
487
513
|
lines.push(` ${teal("──")} ${bold("Git Sync")} ${teal("─".repeat(35))}`);
|
|
@@ -547,6 +573,7 @@ function buildStoppedStatusPayload(socketPath, syncRows = [], agentRows = []) {
|
|
|
547
573
|
},
|
|
548
574
|
senses: [],
|
|
549
575
|
workers: [],
|
|
576
|
+
healthChecks: [],
|
|
550
577
|
sync: syncRows,
|
|
551
578
|
agents: agentRows,
|
|
552
579
|
providers: [],
|
|
@@ -60,6 +60,7 @@ const drift_detection_1 = require("./drift-detection");
|
|
|
60
60
|
const pulse_1 = require("./pulse");
|
|
61
61
|
const socket_client_1 = require("./socket-client");
|
|
62
62
|
const bundle_manifest_1 = require("../../mind/bundle-manifest");
|
|
63
|
+
const mcp_canary_1 = require("./mcp-canary");
|
|
63
64
|
function parseSocketPath(argv) {
|
|
64
65
|
const socketIndex = argv.indexOf("--socket");
|
|
65
66
|
if (socketIndex >= 0) {
|
|
@@ -131,7 +132,22 @@ const senseManager = new sense_manager_1.DaemonSenseManager({
|
|
|
131
132
|
const healthMonitor = new health_monitor_1.HealthMonitor({
|
|
132
133
|
processManager,
|
|
133
134
|
scheduler,
|
|
134
|
-
senseProbeProvider: () =>
|
|
135
|
+
senseProbeProvider: () => [
|
|
136
|
+
...senseManager.listHealthProbes(),
|
|
137
|
+
...managedAgents.map((agent) => (0, mcp_canary_1.createMcpStatusCanaryProbe)({
|
|
138
|
+
agent,
|
|
139
|
+
socketPath,
|
|
140
|
+
command: process.execPath,
|
|
141
|
+
commandArgs: [
|
|
142
|
+
path.join(__dirname, "ouro-bot-entry.js"),
|
|
143
|
+
"mcp-serve",
|
|
144
|
+
"--agent",
|
|
145
|
+
agent,
|
|
146
|
+
"--socket",
|
|
147
|
+
socketPath,
|
|
148
|
+
],
|
|
149
|
+
})),
|
|
150
|
+
],
|
|
135
151
|
alertSink: (message) => {
|
|
136
152
|
(0, runtime_1.emitNervesEvent)({
|
|
137
153
|
level: "error",
|
|
@@ -173,6 +189,10 @@ function scheduleCleanProcessExitAfterStopCommand() {
|
|
|
173
189
|
if (stopCommandExitScheduled)
|
|
174
190
|
return;
|
|
175
191
|
stopCommandExitScheduled = true;
|
|
192
|
+
// Account for the explicit daemon.stop path so the process exit catch-all
|
|
193
|
+
// does not mislabel an operator-requested stop as an unexpected clean exit.
|
|
194
|
+
_tombstoneWritten = true;
|
|
195
|
+
writeStopCommandHealthState();
|
|
176
196
|
setTimeout(() => process.exit(0), 100);
|
|
177
197
|
}
|
|
178
198
|
const daemon = new daemon_1.OuroDaemon({
|
|
@@ -328,6 +348,18 @@ const healthWriter = new daemon_health_1.DaemonHealthWriter((0, daemon_health_1.
|
|
|
328
348
|
const healthSink = (0, daemon_health_1.createHealthNervesSink)(healthWriter, buildDaemonHealthState);
|
|
329
349
|
(0, index_1.registerGlobalLogSink)(healthSink);
|
|
330
350
|
/* v8 ignore stop */
|
|
351
|
+
function writeStopCommandHealthState() {
|
|
352
|
+
try {
|
|
353
|
+
healthWriter.writeHealth({
|
|
354
|
+
...buildDaemonHealthState(),
|
|
355
|
+
status: "down",
|
|
356
|
+
uptimeSeconds: Math.floor(process.uptime()),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// Health writes are best-effort during shutdown.
|
|
361
|
+
}
|
|
362
|
+
}
|
|
331
363
|
/* v8 ignore start -- habit wiring: lambdas delegate to processManager/fs; tested via HabitScheduler unit tests @preserve */
|
|
332
364
|
void daemon.start().then(() => {
|
|
333
365
|
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
@@ -54,8 +54,6 @@ const bundle_meta_1 = require("./hooks/bundle-meta");
|
|
|
54
54
|
const agent_config_v2_1 = require("./hooks/agent-config-v2");
|
|
55
55
|
const bundle_manifest_1 = require("../../mind/bundle-manifest");
|
|
56
56
|
const update_checker_1 = require("../versioning/update-checker");
|
|
57
|
-
const staged_restart_1 = require("../versioning/staged-restart");
|
|
58
|
-
const ouro_version_manager_1 = require("../versioning/ouro-version-manager");
|
|
59
57
|
const child_process_1 = require("child_process");
|
|
60
58
|
const pending_1 = require("../../mind/pending");
|
|
61
59
|
const agent_service_1 = require("./agent-service");
|
|
@@ -356,15 +354,20 @@ function unhealthySenseRows(senses) {
|
|
|
356
354
|
return true;
|
|
357
355
|
});
|
|
358
356
|
}
|
|
359
|
-
function
|
|
357
|
+
function unhealthyHealthChecks(healthChecks) {
|
|
358
|
+
return healthChecks.filter((row) => row.status !== "ok");
|
|
359
|
+
}
|
|
360
|
+
function overviewHealth(workers, senses, healthChecks = []) {
|
|
360
361
|
if (!workers.every((worker) => worker.status === "running"))
|
|
361
362
|
return "warn";
|
|
362
363
|
if (unhealthySenseRows(senses).length > 0)
|
|
363
364
|
return "warn";
|
|
365
|
+
if (unhealthyHealthChecks(healthChecks).length > 0)
|
|
366
|
+
return "warn";
|
|
364
367
|
return "ok";
|
|
365
368
|
}
|
|
366
369
|
function formatStatusSummary(payload) {
|
|
367
|
-
if (payload.overview.workerCount === 0 && payload.overview.senseCount === 0) {
|
|
370
|
+
if (payload.overview.workerCount === 0 && payload.overview.senseCount === 0 && (payload.healthChecks ?? []).length === 0) {
|
|
368
371
|
return "no managed agents";
|
|
369
372
|
}
|
|
370
373
|
const degraded = [
|
|
@@ -373,6 +376,9 @@ function formatStatusSummary(payload) {
|
|
|
373
376
|
.map((row) => `worker:${row.agent}/${row.worker}:${row.status}`),
|
|
374
377
|
...unhealthySenseRows(payload.senses)
|
|
375
378
|
.map((row) => `sense:${row.agent}/${row.sense}:${row.status}`),
|
|
379
|
+
...(payload.healthChecks ?? [])
|
|
380
|
+
.filter((row) => row.status !== "ok")
|
|
381
|
+
.map((row) => `health-check:${row.name}:${row.status}`),
|
|
376
382
|
];
|
|
377
383
|
const detail = degraded.length > 0 ? `\tdegraded=${degraded.join(",")}` : "";
|
|
378
384
|
if (!detail) {
|
|
@@ -498,6 +504,7 @@ class OuroDaemon {
|
|
|
498
504
|
const snapshots = this.processManager.listAgentSnapshots();
|
|
499
505
|
const workers = buildWorkerRows(snapshots);
|
|
500
506
|
const senses = this.senseManager?.listSenseRows() ?? [];
|
|
507
|
+
const healthChecks = this.healthMonitor.getLastResults?.() ?? [];
|
|
501
508
|
const repoRoot = (0, identity_1.getRepoRoot)();
|
|
502
509
|
const sync = (0, agent_discovery_1.listBundleSyncRows)({ bundlesRoot: this.bundlesRoot });
|
|
503
510
|
const agents = (0, agent_discovery_1.listAllBundleAgents)({ bundlesRoot: this.bundlesRoot });
|
|
@@ -509,7 +516,7 @@ class OuroDaemon {
|
|
|
509
516
|
return {
|
|
510
517
|
overview: {
|
|
511
518
|
daemon: "running",
|
|
512
|
-
health: overviewHealth(workers, senses),
|
|
519
|
+
health: overviewHealth(workers, senses, healthChecks),
|
|
513
520
|
socketPath: this.socketPath,
|
|
514
521
|
mailboxUrl,
|
|
515
522
|
outlookUrl: mailboxUrl,
|
|
@@ -521,6 +528,7 @@ class OuroDaemon {
|
|
|
521
528
|
},
|
|
522
529
|
workers,
|
|
523
530
|
senses,
|
|
531
|
+
...(healthChecks.length > 0 ? { healthChecks } : {}),
|
|
524
532
|
sync,
|
|
525
533
|
agents,
|
|
526
534
|
...(providers.length > 0 ? { providers } : {}),
|
|
@@ -562,8 +570,6 @@ class OuroDaemon {
|
|
|
562
570
|
await (0, update_hooks_1.applyPendingUpdates)(this.bundlesRoot, currentVersion);
|
|
563
571
|
// Start periodic update checker (polls npm registry every 30 minutes)
|
|
564
572
|
// Skip in dev mode — dev builds should not auto-update from npm
|
|
565
|
-
const bundlesRoot = this.bundlesRoot;
|
|
566
|
-
const daemonSocketPath = this.socketPath;
|
|
567
573
|
if (this.mode === "dev") {
|
|
568
574
|
(0, runtime_1.emitNervesEvent)({
|
|
569
575
|
component: "daemon",
|
|
@@ -573,7 +579,6 @@ class OuroDaemon {
|
|
|
573
579
|
});
|
|
574
580
|
}
|
|
575
581
|
else {
|
|
576
|
-
const daemon = this;
|
|
577
582
|
(0, update_checker_1.startUpdateChecker)({
|
|
578
583
|
currentVersion,
|
|
579
584
|
deps: {
|
|
@@ -583,55 +588,6 @@ class OuroDaemon {
|
|
|
583
588
|
return res.json();
|
|
584
589
|
},
|
|
585
590
|
},
|
|
586
|
-
onUpdate: /* v8 ignore start -- integration: real npm install + process spawn @preserve */ async (result) => {
|
|
587
|
-
if (!result.latestVersion)
|
|
588
|
-
return;
|
|
589
|
-
// Install via the version manager (NOT `npm install -g`). The
|
|
590
|
-
// global install path doesn't end up on the daemon process's
|
|
591
|
-
// NODE_PATH, so the previous `require.resolve('@ouro.bot/cli')`
|
|
592
|
-
// -based path lookup always returned null and the staged restart
|
|
593
|
-
// never actually completed. Verified live on 2026-04-08:
|
|
594
|
-
// alpha.268 daemon detected alpha.270 was available, ran the
|
|
595
|
-
// staged restart, and bailed at `staged_restart_path_failed` —
|
|
596
|
-
// meaning the daemon could never auto-update itself and required
|
|
597
|
-
// manual `ouro up` to pick up new versions.
|
|
598
|
-
//
|
|
599
|
-
// Switch to the version-managed layout the CLI itself uses:
|
|
600
|
-
// installVersion(version) puts files at
|
|
601
|
-
// ~/.ouro-cli/versions/{version}/node_modules/@ouro.bot/cli
|
|
602
|
-
// which is a known path we can compute deterministically.
|
|
603
|
-
// Then activateVersion(version) flips the CurrentVersion symlink
|
|
604
|
-
// so the next `ouro up` from the user sees the same version
|
|
605
|
-
// the daemon is running.
|
|
606
|
-
const cliHome = (0, ouro_version_manager_1.getOuroCliHome)();
|
|
607
|
-
await (0, staged_restart_1.performStagedRestart)(result.latestVersion, {
|
|
608
|
-
execSync: (cmd) => (0, child_process_1.execSync)(cmd, { stdio: "inherit" }),
|
|
609
|
-
spawnSync: child_process_1.spawnSync,
|
|
610
|
-
installNewVersion: (version) => {
|
|
611
|
-
(0, ouro_version_manager_1.installVersion)(version, {});
|
|
612
|
-
(0, ouro_version_manager_1.activateVersion)(version, {});
|
|
613
|
-
},
|
|
614
|
-
resolveNewCodePath: (version) => {
|
|
615
|
-
const versionPath = path.join(cliHome, "versions", version, "node_modules", "@ouro.bot", "cli");
|
|
616
|
-
return fs.existsSync(versionPath) ? versionPath : null;
|
|
617
|
-
},
|
|
618
|
-
gracefulShutdown: () => daemon.stop(),
|
|
619
|
-
spawnNewDaemon: (entryPath, sock) => {
|
|
620
|
-
const outFd = fs.openSync(os.devNull, "w");
|
|
621
|
-
const errFd = fs.openSync(os.devNull, "w");
|
|
622
|
-
const child = (0, child_process_1.spawn)(process.execPath, [entryPath, "--socket", sock], {
|
|
623
|
-
detached: true,
|
|
624
|
-
stdio: ["ignore", outFd, errFd],
|
|
625
|
-
});
|
|
626
|
-
child.unref();
|
|
627
|
-
return { pid: child.pid ?? null };
|
|
628
|
-
},
|
|
629
|
-
nodePath: process.execPath,
|
|
630
|
-
bundlesRoot,
|
|
631
|
-
socketPath: daemonSocketPath,
|
|
632
|
-
});
|
|
633
|
-
},
|
|
634
|
-
/* v8 ignore stop */
|
|
635
591
|
});
|
|
636
592
|
}
|
|
637
593
|
// MCP connections are lazily initialized per-agent during senseTurn
|
|
@@ -1136,6 +1092,7 @@ class OuroDaemon {
|
|
|
1136
1092
|
sessionId: command.sessionId,
|
|
1137
1093
|
taskRef: command.taskRef,
|
|
1138
1094
|
});
|
|
1095
|
+
await this.processManager.startAgent(command.to);
|
|
1139
1096
|
this.processManager.sendToAgent?.(command.to, { type: "message" });
|
|
1140
1097
|
return { ok: true, message: `queued message ${receipt.id}`, data: receipt };
|
|
1141
1098
|
}
|
|
@@ -12,6 +12,7 @@ class HealthMonitor {
|
|
|
12
12
|
senseProbes;
|
|
13
13
|
senseProbeProvider;
|
|
14
14
|
intervalHandle = null;
|
|
15
|
+
lastResults = [];
|
|
15
16
|
constructor(options) {
|
|
16
17
|
this.processManager = options.processManager;
|
|
17
18
|
this.scheduler = options.scheduler;
|
|
@@ -177,6 +178,7 @@ class HealthMonitor {
|
|
|
177
178
|
});
|
|
178
179
|
}
|
|
179
180
|
}
|
|
181
|
+
this.lastResults = results.map((result) => ({ ...result }));
|
|
180
182
|
for (const result of results) {
|
|
181
183
|
(0, runtime_1.emitNervesEvent)({
|
|
182
184
|
level: result.status === "critical" ? "error" : result.status === "warn" ? "warn" : "info",
|
|
@@ -191,5 +193,8 @@ class HealthMonitor {
|
|
|
191
193
|
}
|
|
192
194
|
return results;
|
|
193
195
|
}
|
|
196
|
+
getLastResults() {
|
|
197
|
+
return this.lastResults.map((result) => ({ ...result }));
|
|
198
|
+
}
|
|
194
199
|
}
|
|
195
200
|
exports.HealthMonitor = HealthMonitor;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseMcpStatusText = parseMcpStatusText;
|
|
37
|
+
exports.runMcpStatusCanary = runMcpStatusCanary;
|
|
38
|
+
exports.formatMcpStatusCanaryResult = formatMcpStatusCanaryResult;
|
|
39
|
+
exports.createMcpStatusCanaryProbe = createMcpStatusCanaryProbe;
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
43
|
+
const DEFAULT_CANARY_TIMEOUT_MS = 10_000;
|
|
44
|
+
const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
45
|
+
function defaultCommandArgs(agent, socketPath) {
|
|
46
|
+
const entryPath = path.join(__dirname, "ouro-bot-entry.js");
|
|
47
|
+
return [
|
|
48
|
+
entryPath,
|
|
49
|
+
"mcp-serve",
|
|
50
|
+
"--agent",
|
|
51
|
+
agent,
|
|
52
|
+
...(socketPath ? ["--socket", socketPath] : []),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
function responseText(response) {
|
|
56
|
+
const result = response.result;
|
|
57
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
58
|
+
return JSON.stringify(response);
|
|
59
|
+
const content = result.content;
|
|
60
|
+
if (!Array.isArray(content))
|
|
61
|
+
return JSON.stringify(response);
|
|
62
|
+
const first = content[0];
|
|
63
|
+
if (!first || typeof first !== "object" || Array.isArray(first))
|
|
64
|
+
return JSON.stringify(response);
|
|
65
|
+
const text = first.text;
|
|
66
|
+
return typeof text === "string" ? text : JSON.stringify(response);
|
|
67
|
+
}
|
|
68
|
+
function parseFields(line) {
|
|
69
|
+
const parsed = {};
|
|
70
|
+
for (const segment of line.split("\t")) {
|
|
71
|
+
const idx = segment.indexOf("=");
|
|
72
|
+
if (idx <= 0)
|
|
73
|
+
continue;
|
|
74
|
+
parsed[segment.slice(0, idx)] = segment.slice(idx + 1);
|
|
75
|
+
}
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
function parseMcpStatusText(text) {
|
|
79
|
+
const daemon = {};
|
|
80
|
+
const senses = {};
|
|
81
|
+
for (const line of text.split(/\r?\n/)) {
|
|
82
|
+
const trimmed = line.trim();
|
|
83
|
+
if (!trimmed)
|
|
84
|
+
continue;
|
|
85
|
+
if (trimmed.startsWith("daemon=")) {
|
|
86
|
+
Object.assign(daemon, parseFields(trimmed));
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!trimmed.startsWith("sense="))
|
|
90
|
+
continue;
|
|
91
|
+
const fields = parseFields(trimmed);
|
|
92
|
+
const sense = fields.sense;
|
|
93
|
+
if (!sense)
|
|
94
|
+
continue;
|
|
95
|
+
const [name, status = "unknown"] = sense.split(":");
|
|
96
|
+
senses[name] = { ...fields, name, status };
|
|
97
|
+
}
|
|
98
|
+
return { daemon, senses, raw: text };
|
|
99
|
+
}
|
|
100
|
+
function validateMcpStatus(parsed, requiredSenses) {
|
|
101
|
+
const failures = [];
|
|
102
|
+
if (parsed.daemon.daemon !== "running") {
|
|
103
|
+
failures.push(`daemon=${parsed.daemon.daemon ?? "missing"}`);
|
|
104
|
+
}
|
|
105
|
+
if (parsed.daemon.health !== "ok") {
|
|
106
|
+
failures.push(`health=${parsed.daemon.health ?? "missing"}`);
|
|
107
|
+
}
|
|
108
|
+
if (parsed.daemon.daemonVersion &&
|
|
109
|
+
parsed.daemon.mcpVersion &&
|
|
110
|
+
parsed.daemon.daemonVersion !== parsed.daemon.mcpVersion) {
|
|
111
|
+
failures.push(`version mismatch daemon=${parsed.daemon.daemonVersion} mcp=${parsed.daemon.mcpVersion}`);
|
|
112
|
+
}
|
|
113
|
+
for (const [sense, row] of Object.entries(parsed.senses)) {
|
|
114
|
+
if (row.status === "disabled")
|
|
115
|
+
continue;
|
|
116
|
+
if (row.status === "running" || row.status === "interactive")
|
|
117
|
+
continue;
|
|
118
|
+
failures.push(`sense=${sense}:${row.status}`);
|
|
119
|
+
}
|
|
120
|
+
for (const sense of requiredSenses) {
|
|
121
|
+
const row = parsed.senses[sense];
|
|
122
|
+
if (!row) {
|
|
123
|
+
failures.push(`required sense missing: ${sense}`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (row.status !== "running" && row.status !== "interactive") {
|
|
127
|
+
failures.push(`required sense unhealthy: ${sense}:${row.status}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const senseSummary = Object.values(parsed.senses)
|
|
131
|
+
.map((row) => `${row.name}:${row.status}`)
|
|
132
|
+
.join(",");
|
|
133
|
+
const summary = failures.length === 0
|
|
134
|
+
? `mcp canary ok: daemon=${parsed.daemon.daemon} health=${parsed.daemon.health} senses=${senseSummary}`
|
|
135
|
+
: `mcp canary failed: ${failures.join("; ")}`;
|
|
136
|
+
return {
|
|
137
|
+
ok: failures.length === 0,
|
|
138
|
+
summary,
|
|
139
|
+
details: failures.length === 0 ? [parsed.raw] : [...failures, parsed.raw],
|
|
140
|
+
parsed,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function runMcpStatusCanary(options) {
|
|
144
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_CANARY_TIMEOUT_MS;
|
|
145
|
+
/* v8 ignore next -- default spawn is exercised by live canaries, while unit tests inject a fake child @preserve */
|
|
146
|
+
const spawnImpl = options.spawnImpl ?? child_process_1.spawn;
|
|
147
|
+
const command = options.command ?? process.execPath;
|
|
148
|
+
const commandArgs = options.commandArgs ?? defaultCommandArgs(options.agent, options.socketPath);
|
|
149
|
+
const requiredSenses = options.requiredSenses ?? [];
|
|
150
|
+
(0, runtime_1.emitNervesEvent)({
|
|
151
|
+
component: "daemon",
|
|
152
|
+
event: "daemon.mcp_canary_start",
|
|
153
|
+
message: "starting MCP status canary",
|
|
154
|
+
meta: { agent: options.agent, command, commandArgs, timeoutMs, requiredSenses },
|
|
155
|
+
});
|
|
156
|
+
const child = spawnImpl(command, commandArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
157
|
+
let buffer = "";
|
|
158
|
+
let stderr = "";
|
|
159
|
+
const pending = new Map();
|
|
160
|
+
function cleanup() {
|
|
161
|
+
for (const [, request] of pending) {
|
|
162
|
+
clearTimeout(request.timer);
|
|
163
|
+
}
|
|
164
|
+
pending.clear();
|
|
165
|
+
if (!child.killed)
|
|
166
|
+
child.kill();
|
|
167
|
+
}
|
|
168
|
+
function failAll(error) {
|
|
169
|
+
for (const [, request] of pending) {
|
|
170
|
+
clearTimeout(request.timer);
|
|
171
|
+
request.reject(error);
|
|
172
|
+
}
|
|
173
|
+
pending.clear();
|
|
174
|
+
}
|
|
175
|
+
child.stderr?.setEncoding("utf8");
|
|
176
|
+
child.stderr?.on("data", (chunk) => {
|
|
177
|
+
stderr += chunk.toString();
|
|
178
|
+
});
|
|
179
|
+
child.stdout?.setEncoding("utf8");
|
|
180
|
+
child.stdout?.on("data", (chunk) => {
|
|
181
|
+
buffer += chunk.toString();
|
|
182
|
+
for (;;) {
|
|
183
|
+
const idx = buffer.indexOf("\n");
|
|
184
|
+
if (idx === -1)
|
|
185
|
+
break;
|
|
186
|
+
const line = buffer.slice(0, idx).trim();
|
|
187
|
+
buffer = buffer.slice(idx + 1);
|
|
188
|
+
if (!line)
|
|
189
|
+
continue;
|
|
190
|
+
let response;
|
|
191
|
+
try {
|
|
192
|
+
response = JSON.parse(line);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
failAll(new Error(`MCP canary received malformed JSON: ${line}`));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const id = typeof response.id === "number" ? response.id : null;
|
|
199
|
+
if (id === null)
|
|
200
|
+
continue;
|
|
201
|
+
const request = pending.get(id);
|
|
202
|
+
if (!request)
|
|
203
|
+
continue;
|
|
204
|
+
pending.delete(id);
|
|
205
|
+
clearTimeout(request.timer);
|
|
206
|
+
request.resolve(response);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
child.on("error", (error) => failAll(error));
|
|
210
|
+
child.on("close", (code, signal) => {
|
|
211
|
+
if (pending.size === 0)
|
|
212
|
+
return;
|
|
213
|
+
failAll(new Error(`MCP canary process closed before response code=${code} signal=${signal ?? "none"} stderr=${stderr.trim()}`));
|
|
214
|
+
});
|
|
215
|
+
let nextId = 1;
|
|
216
|
+
function request(method, params) {
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
if (!child.stdin?.writable) {
|
|
219
|
+
reject(new Error("MCP canary stdin is not writable"));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const id = nextId++;
|
|
223
|
+
const timer = setTimeout(() => {
|
|
224
|
+
pending.delete(id);
|
|
225
|
+
reject(new Error(`MCP canary timed out waiting for ${method}; stderr=${stderr.trim()}`));
|
|
226
|
+
}, timeoutMs);
|
|
227
|
+
pending.set(id, { resolve, reject, timer });
|
|
228
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
await request("initialize", {
|
|
233
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
234
|
+
capabilities: {},
|
|
235
|
+
clientInfo: { name: "ouro-mcp-canary", version: "1.0" },
|
|
236
|
+
});
|
|
237
|
+
child.stdin?.write(JSON.stringify({ jsonrpc: "2.0", method: "initialized" }) + "\n");
|
|
238
|
+
const statusResponse = await request("tools/call", {
|
|
239
|
+
name: "status",
|
|
240
|
+
arguments: {},
|
|
241
|
+
});
|
|
242
|
+
const result = statusResponse.result;
|
|
243
|
+
if (result && typeof result === "object" && !Array.isArray(result) && result.isError === true) {
|
|
244
|
+
throw new Error(responseText(statusResponse));
|
|
245
|
+
}
|
|
246
|
+
const parsed = parseMcpStatusText(responseText(statusResponse));
|
|
247
|
+
const canary = validateMcpStatus(parsed, requiredSenses);
|
|
248
|
+
(0, runtime_1.emitNervesEvent)({
|
|
249
|
+
component: "daemon",
|
|
250
|
+
event: canary.ok ? "daemon.mcp_canary_end" : "daemon.mcp_canary_error",
|
|
251
|
+
level: canary.ok ? "info" : "error",
|
|
252
|
+
message: canary.summary,
|
|
253
|
+
meta: { agent: options.agent, ok: canary.ok },
|
|
254
|
+
});
|
|
255
|
+
return canary;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
259
|
+
(0, runtime_1.emitNervesEvent)({
|
|
260
|
+
component: "daemon",
|
|
261
|
+
event: "daemon.mcp_canary_error",
|
|
262
|
+
level: "error",
|
|
263
|
+
message: "MCP status canary failed",
|
|
264
|
+
meta: { agent: options.agent, reason },
|
|
265
|
+
});
|
|
266
|
+
return { ok: false, summary: `mcp canary failed: ${reason}`, details: [reason] };
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
child.stdin?.end();
|
|
270
|
+
cleanup();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function formatMcpStatusCanaryResult(result) {
|
|
274
|
+
return [
|
|
275
|
+
result.ok ? "mcp canary: ok" : "mcp canary: failed",
|
|
276
|
+
result.summary,
|
|
277
|
+
...result.details.map((line) => ` ${line}`),
|
|
278
|
+
].join("\n");
|
|
279
|
+
}
|
|
280
|
+
function createMcpStatusCanaryProbe(options) {
|
|
281
|
+
return {
|
|
282
|
+
name: `mcp-canary:${options.agent}`,
|
|
283
|
+
check: async () => {
|
|
284
|
+
const result = await runMcpStatusCanary(options);
|
|
285
|
+
return { ok: result.ok, detail: result.summary };
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.McpClient = void 0;
|
|
4
|
+
exports.isMcpTransportError = isMcpTransportError;
|
|
4
5
|
const child_process_1 = require("child_process");
|
|
5
6
|
const readline_1 = require("readline");
|
|
6
7
|
const runtime_1 = require("../nerves/runtime");
|
|
7
8
|
const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
9
|
+
const DEFAULT_REQUEST_TIMEOUT = 10_000;
|
|
8
10
|
const DEFAULT_TOOL_CALL_TIMEOUT = 30_000;
|
|
11
|
+
function isMcpTransportError(error) {
|
|
12
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13
|
+
const normalized = message.toLowerCase();
|
|
14
|
+
return normalized.includes("disconnected")
|
|
15
|
+
|| normalized.includes("transport")
|
|
16
|
+
|| normalized.includes("closed")
|
|
17
|
+
|| normalized.includes("econnreset")
|
|
18
|
+
|| normalized.includes("econnrefused")
|
|
19
|
+
|| normalized.includes("enoent")
|
|
20
|
+
|| normalized.includes("epipe")
|
|
21
|
+
|| normalized.includes("broken pipe")
|
|
22
|
+
|| normalized.includes("not writable");
|
|
23
|
+
}
|
|
9
24
|
class McpClient {
|
|
10
25
|
config;
|
|
11
26
|
process = null;
|
|
@@ -18,6 +33,9 @@ class McpClient {
|
|
|
18
33
|
this.config = config;
|
|
19
34
|
}
|
|
20
35
|
async connect() {
|
|
36
|
+
if (this.connected)
|
|
37
|
+
return;
|
|
38
|
+
this.shutdownProcessOnly();
|
|
21
39
|
(0, runtime_1.emitNervesEvent)({
|
|
22
40
|
event: "mcp.connect_start",
|
|
23
41
|
component: "repertoire",
|
|
@@ -44,6 +62,7 @@ class McpClient {
|
|
|
44
62
|
}
|
|
45
63
|
catch (error) {
|
|
46
64
|
this.connected = false;
|
|
65
|
+
this.shutdownProcessOnly();
|
|
47
66
|
(0, runtime_1.emitNervesEvent)({
|
|
48
67
|
level: "error",
|
|
49
68
|
event: "mcp.connect_error",
|
|
@@ -76,6 +95,10 @@ class McpClient {
|
|
|
76
95
|
this.cachedTools = allTools;
|
|
77
96
|
return allTools;
|
|
78
97
|
}
|
|
98
|
+
async refreshTools() {
|
|
99
|
+
this.cachedTools = null;
|
|
100
|
+
return this.listTools();
|
|
101
|
+
}
|
|
79
102
|
async callTool(name, args, timeout = DEFAULT_TOOL_CALL_TIMEOUT) {
|
|
80
103
|
(0, runtime_1.emitNervesEvent)({
|
|
81
104
|
event: "mcp.tool_call_start",
|
|
@@ -139,7 +162,7 @@ class McpClient {
|
|
|
139
162
|
});
|
|
140
163
|
return result;
|
|
141
164
|
}
|
|
142
|
-
sendRequest(method, params, timeout) {
|
|
165
|
+
sendRequest(method, params, timeout = DEFAULT_REQUEST_TIMEOUT) {
|
|
143
166
|
return new Promise((resolve, reject) => {
|
|
144
167
|
if (!this.process || !this.connected && method !== "initialize") {
|
|
145
168
|
reject(new Error("MCP client is disconnected"));
|
|
@@ -160,14 +183,21 @@ class McpClient {
|
|
|
160
183
|
method,
|
|
161
184
|
params,
|
|
162
185
|
};
|
|
163
|
-
this.writeMessage(request)
|
|
186
|
+
if (!this.writeMessage(request)) {
|
|
187
|
+
this.pending.delete(id);
|
|
188
|
+
if (pending.timer) {
|
|
189
|
+
clearTimeout(pending.timer);
|
|
190
|
+
}
|
|
191
|
+
reject(new Error(`MCP transport is not writable for request: ${method}`));
|
|
192
|
+
}
|
|
164
193
|
});
|
|
165
194
|
}
|
|
166
195
|
writeMessage(message) {
|
|
167
|
-
/* v8 ignore next -- defensive: stdin always writable during active connection @preserve */
|
|
168
196
|
if (this.process?.stdin?.writable) {
|
|
169
197
|
this.process.stdin.write(JSON.stringify(message) + "\n");
|
|
198
|
+
return true;
|
|
170
199
|
}
|
|
200
|
+
return false;
|
|
171
201
|
}
|
|
172
202
|
setupLineReader() {
|
|
173
203
|
/* v8 ignore next -- defensive: stdout always exists after spawn @preserve */
|
|
@@ -251,5 +281,13 @@ class McpClient {
|
|
|
251
281
|
this.pending.delete(id);
|
|
252
282
|
}
|
|
253
283
|
}
|
|
284
|
+
shutdownProcessOnly() {
|
|
285
|
+
this.rejectAllPending(new Error("MCP transport closed during reconnect"));
|
|
286
|
+
/* v8 ignore next -- defensive: process may already be absent @preserve */
|
|
287
|
+
if (this.process && !this.process.killed) {
|
|
288
|
+
this.process.kill();
|
|
289
|
+
}
|
|
290
|
+
this.process = null;
|
|
291
|
+
}
|
|
254
292
|
}
|
|
255
293
|
exports.McpClient = McpClient;
|
|
@@ -33,14 +33,59 @@ class McpManager {
|
|
|
33
33
|
return result;
|
|
34
34
|
}
|
|
35
35
|
async callTool(server, tool, args) {
|
|
36
|
-
|
|
36
|
+
let entry = this.servers.get(server);
|
|
37
37
|
if (!entry) {
|
|
38
38
|
throw new Error(`Unknown server: ${server}`);
|
|
39
39
|
}
|
|
40
40
|
if (!entry.client.isConnected()) {
|
|
41
|
-
|
|
41
|
+
await this.recoverStaleTransport(server, "pre-call disconnected");
|
|
42
|
+
entry = this.servers.get(server);
|
|
43
|
+
if (!entry?.client.isConnected()) {
|
|
44
|
+
throw new Error(`Server "${server}" is disconnected`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return await entry.client.callTool(tool, args);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!(0, mcp_client_1.isMcpTransportError)(error)) {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
55
|
+
await this.recoverStaleTransport(server, reason);
|
|
56
|
+
const recovered = this.servers.get(server);
|
|
57
|
+
if (!recovered?.client.isConnected()) {
|
|
58
|
+
throw new Error(`Server "${server}" is disconnected after recovery: ${reason}`);
|
|
59
|
+
}
|
|
60
|
+
return recovered.client.callTool(tool, args);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async runCanaries() {
|
|
64
|
+
const results = [];
|
|
65
|
+
for (const [server, entry] of [...this.servers]) {
|
|
66
|
+
try {
|
|
67
|
+
if (!entry.client.isConnected()) {
|
|
68
|
+
await this.recoverStaleTransport(server, "canary disconnected");
|
|
69
|
+
}
|
|
70
|
+
const current = this.servers.get(server);
|
|
71
|
+
if (!current?.client.isConnected()) {
|
|
72
|
+
results.push({ server, ok: false, detail: "disconnected after recovery attempt" });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const tools = await current.client.refreshTools();
|
|
76
|
+
current.cachedTools = tools;
|
|
77
|
+
current.consecutiveFailures = 0;
|
|
78
|
+
results.push({ server, ok: true, detail: `${tools.length} tools listed` });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
82
|
+
if ((0, mcp_client_1.isMcpTransportError)(error)) {
|
|
83
|
+
await this.recoverStaleTransport(server, reason);
|
|
84
|
+
}
|
|
85
|
+
results.push({ server, ok: false, detail: reason });
|
|
86
|
+
}
|
|
42
87
|
}
|
|
43
|
-
return
|
|
88
|
+
return results;
|
|
44
89
|
}
|
|
45
90
|
/* v8 ignore start — reconcile: dynamic MCP server management, tested via integration @preserve */
|
|
46
91
|
/** Re-read agent config and connect new servers / disconnect removed ones. */
|
|
@@ -232,6 +277,7 @@ class McpManager {
|
|
|
232
277
|
return;
|
|
233
278
|
// Remove old entry and reconnect
|
|
234
279
|
this.servers.delete(name);
|
|
280
|
+
entry.client.shutdown();
|
|
235
281
|
await this.connectServer(name, entry.config);
|
|
236
282
|
// Preserve failure count
|
|
237
283
|
const newEntry = this.servers.get(name);
|
|
@@ -239,6 +285,17 @@ class McpManager {
|
|
|
239
285
|
newEntry.consecutiveFailures = entry.consecutiveFailures;
|
|
240
286
|
}
|
|
241
287
|
}
|
|
288
|
+
/* v8 ignore stop */
|
|
289
|
+
async recoverStaleTransport(name, reason) {
|
|
290
|
+
(0, runtime_1.emitNervesEvent)({
|
|
291
|
+
level: "warn",
|
|
292
|
+
event: "mcp.transport_recovery",
|
|
293
|
+
component: "repertoire",
|
|
294
|
+
message: `recovering stale MCP transport: ${name}`,
|
|
295
|
+
meta: { server: name, reason },
|
|
296
|
+
});
|
|
297
|
+
await this.restartServer(name);
|
|
298
|
+
}
|
|
242
299
|
}
|
|
243
300
|
exports.McpManager = McpManager;
|
|
244
301
|
let _sharedManager = null;
|