Add warm agent pool example#189
Conversation
Add a pool-manager mcp service that discovers code-reviewer workers via DHT (find_remote_tools) and leases them out through acquire_worker/release_worker, with fencing tokens and grace-eviction so a busy worker is never freed early. Add a single-flight backstop to the reviewer, a headless orchestrator plus sample files, and wire node-b/c/d as a reviewer pool with node-e as the manager in the kind dev mesh.
…de (:9099) Make the local sam-node (make kind-local-node) the base orchestrator entry: default SAM_NODE_URL to http://127.0.0.1:9099/mcp and document make kind-up + make kind-local-node instead of port-forwarding node-a.
Create a fresh McpServer + transport per POST; the SDK forbids connecting one server to multiple transports.
Derive kind image/container name from the service basename so the nested service path stays a valid k8s container name.
… samples The headless orchestrator method is going away. Fold the run guide (Claude Code-driven flow) and lease-enforcement docs into code-reviewer-pool/README.md and move the sample files alongside the services.
Drop the SAM_POOL_SECRET opt-in guards so the manager always mints a token and the reviewer always requires one. Both sides default to a shared hardcoded dev secret (sam-dev-pool-secret) so kind enforces out of the box with no env wiring; SAM_POOL_SECRET still overrides it.
…kind mesh to bare nodes Add a harness-agnostic "Use Cases" docs section (site/content/docs/use-cases/) whose first entry explains the warm agent pool and how to try it on kind. Make the kind mesh-config.yaml ship all five nodes bare so the user decides what to host. Since the e2e check needs calc-mcp, add an overridable MESH_CONFIG in run.sh and a mesh-config.e2e.yaml that pins it; the CI kind-up step selects it. Update the Kubernetes/kind local-testing guide to match (five bare nodes, the MESH_CONFIG override flow).
Remove the standalone docker-compose path (docker-compose.yaml, env.example, and the compose-only sam-node.yaml) and the README. The example is meant to run only inside the mesh/kind sandbox, which uses sam-node-config.yaml.
There was a problem hiding this comment.
Code Review
This pull request introduces a 'Warm Agent Pool' architecture under development/examples/code-reviewer-pool/, replacing the single code-reviewer server with a manager-worker model that leases workers using signed HMAC tokens. It also expands the local Kubernetes development mesh from three to five nodes and updates the documentation. The reviewer feedback highlights several robustness improvements: validating tool call responses before parsing them, adding error listeners to child.stdin when spawning subprocesses to prevent unhandled crashes, and wrapping asynchronous server initialization in a try-catch block to handle startup failures gracefully.
| const res = await node.callTool({ name: "find_remote_tools", arguments: {} }); | ||
| const rows = JSON.parse(res.content[0].text); // [{peer_id, tool_name, ...}] |
There was a problem hiding this comment.
Accessing res.content[0].text directly without checking if res.content is defined and non-empty can lead to a TypeError if the tool call returns no content or fails. It is safer to validate the presence of res.content before parsing.
const res = await node.callTool({ name: "find_remote_tools", arguments: {} });
if (!res.content || res.content.length === 0) {
throw new Error("No content returned from find_remote_tools");
}
const rows = JSON.parse(res.content[0].text); // [{peer_id, tool_name, ...}]| let stderr = ""; | ||
| child.stdout.on("data", (d) => { stdout += d; }); | ||
| child.stderr.on("data", (d) => { stderr += d; }); | ||
| child.on("error", reject); |
There was a problem hiding this comment.
When spawning a child process and writing to its stdin, an unhandled EPIPE or ERR_STREAM_WRITE_AFTER_END error can occur if the process fails to start or exits prematurely. Adding an error listener to child.stdin prevents potential unhandled crashes.
child.on("error", reject);
child.stdin.on("error", reject);| app.listen(PORT, "0.0.0.0", async () => { | ||
| console.log(`pool-manager MCP server on :${PORT}/mcp (pooling '${POOL_SERVICE}')`); | ||
| const transport = new StreamableHTTPClientTransport(new URL(NODE_URL), { | ||
| requestInit: { headers: { Authorization: `Bearer ${API_TOKEN}` } }, | ||
| }); | ||
| await node.connect(transport); | ||
| await discover(); | ||
| setInterval(discover, DISCOVERY_MS); | ||
| }); |
There was a problem hiding this comment.
The asynchronous initialization logic inside app.listen is not wrapped in a try-catch block. If node.connect or discover fails during startup, it will result in an unhandled promise rejection, which can leave the server in a semi-broken state or crash the process. Wrapping this in a try-catch block ensures the error is handled gracefully.
| app.listen(PORT, "0.0.0.0", async () => { | |
| console.log(`pool-manager MCP server on :${PORT}/mcp (pooling '${POOL_SERVICE}')`); | |
| const transport = new StreamableHTTPClientTransport(new URL(NODE_URL), { | |
| requestInit: { headers: { Authorization: `Bearer ${API_TOKEN}` } }, | |
| }); | |
| await node.connect(transport); | |
| await discover(); | |
| setInterval(discover, DISCOVERY_MS); | |
| }); | |
| app.listen(PORT, "0.0.0.0", async () => { | |
| console.log("pool-manager MCP server on :" + PORT + "/mcp (pooling '" + POOL_SERVICE + "')"); | |
| try { | |
| const transport = new StreamableHTTPClientTransport(new URL(NODE_URL), { | |
| requestInit: { headers: { Authorization: `Bearer ${API_TOKEN}` } }, | |
| }); | |
| await node.connect(transport); | |
| await discover(); | |
| setInterval(discover, DISCOVERY_MS); | |
| } catch (err) { | |
| console.error("Failed to initialize MCP client connection: " + (err?.message ?? err)); | |
| } | |
| }); |
No description provided.