Let the TypeScript mesh client take every role's tools from the console on its machine, so it asks the controller for none (issue 1955, ADR 0571 clause 5)

The Go console answers mesh/seats beside MCP's methods from mesh-sdk's seat
definitions and placement's seat-holders. The console test that runs bundles
expects the unread seat-holders line beside ghost.
This commit is contained in:
2026-10-29 23:19:55 +01:00
parent 67850457ef
commit 200249a84f
8 changed files with 140 additions and 36 deletions
+33 -15
View File
@@ -26,9 +26,28 @@ import { TOOLS_VERB, type ToolsAnswer } from "./runtime.js";
/** Where the catalogue answers which modules the mesh holds. */
const CATALOGUE_MODULES = "mesh-catalog.catalog_modules";
/** Where the mesh answers every role's tools, from its records: the mesh-controller seat's own
* `tools` verb (novox/hq ADR 0154, design 33 §5). */
const SEAT_TOOLS = "seat:mesh-controller.tools";
/** Where every role's tools are answered: the console on this machine, the Go runtime, which holds mesh-sdk's seat
* definitions and reads who holds each seat from placement (novox/hq ADR 0571 clauses 4 and 5). Asked over the loopback
* channel `mesh --console` already uses, by the method `mesh/seats` beside MCP's; never the controller. */
export const SEATS_METHOD = "mesh/seats";
const DEFAULT_CONSOLE = "http://127.0.0.1:4270";
type SeatsAnswer = { seats?: { seat: string; scope?: string; tools?: ToolsAnswer["tools"] }[]; holders_why?: string };
/** Every role's tools from the console on this machine. */
async function seatsFromConsole(consoleUrl: string): Promise<SeatsAnswer> {
const endpoint = consoleUrl.endsWith("/mcp") ? consoleUrl : `${consoleUrl.replace(/\/$/, "")}/mcp`;
const res = await fetch(endpoint, {
method: "POST",
headers: { "content-type": "application/json", accept: "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: SEATS_METHOD }),
signal: AbortSignal.timeout(5_000),
});
if (!res.ok) throw new Error(`the console at ${endpoint} answered ${res.status}`);
const reply = (await res.json()) as { result?: SeatsAnswer; error?: { message: string } };
if (reply.error) throw new Error(reply.error.message);
return reply.result ?? {};
}
/** A tool as its module describes it. */
export interface Tool {
@@ -58,8 +77,8 @@ export interface Tool {
export interface Listing {
tools: Tool[];
/** Modules the catalogue holds whose runtime did not answer `tools`: not assigned, not up, or built
* before the runtime answered it. Each may still be called by name. The mesh's own records are
* listed here as `mesh-controller (seat)` when the control plane did not answer. */
* before the runtime answered it. Each may still be called by name. The roles' tools are listed here, with the
* reason, when the console on this machine did not answer them. */
notAnswering: string[];
}
@@ -159,17 +178,16 @@ export async function connectAsTheConsole(path: string): Promise<{ bus: Broker;
* list that goes stale the first time a module is assigned. Every module is asked at once, and the bus
* refuses at once a request nothing serves, so the cost is bounded by the modules that are up.
*/
export async function toolsOn(bus: Broker): Promise<Listing> {
export async function toolsOn(bus: Broker, consoleUrl: string = process.env.MESH_CONSOLE || DEFAULT_CONSOLE): Promise<Listing> {
let rolesWhy = "";
const [answered, roles] = await Promise.all([
bus.request<Record<string, never>, { modules?: { module: string }[] }>(CATALOGUE_MODULES, {}),
// The roles' tools, from the mesh's records (design 33 §5). Asked beside the modules rather
// than first: a control plane that is restarting must not hide every module's tools with it.
bus
.request<Record<string, never>, { seats?: { seat: string; scope?: string; tools?: ToolsAnswer["tools"] }[] }>(
SEAT_TOOLS,
{},
)
.catch(() => undefined),
// The roles' tools, from the console on this machine (ADR 0571). Asked beside the modules rather than first: a
// console that is restarting must not hide every module's tools with it.
seatsFromConsole(consoleUrl).catch((e: unknown) => {
rolesWhy = e instanceof Error ? e.message : String(e);
return undefined;
}),
]);
const names = (answered.modules ?? []).map((m) => m.module).filter((m) => typeof m === "string");
@@ -188,7 +206,7 @@ export async function toolsOn(bus: Broker): Promise<Listing> {
}
}
} else {
notAnswering.push("mesh-controller (seat)");
notAnswering.push(`the roles' tools (the console at ${consoleUrl} did not answer ${SEATS_METHOD}: ${rolesWhy})`);
}
asked.forEach((outcome, i) => {
const module = names[i]!;