diff --git a/modules/claude-code/README.md b/modules/claude-code/README.md index 2575ad9..4111cb6 100644 --- a/modules/claude-code/README.md +++ b/modules/claude-code/README.md @@ -33,7 +33,7 @@ this node a subscription token. Nothing else under the home is read or written. Everything between this module and the rest of the mesh is NATS, in three kinds: an **event** says that something happened and carries no secret, because a stream keeps it; a **request** carries a token, because nothing keeps it (hq design 32 §10); and **state** is the current value of something every node -must see, a node that joins later included — kept, so it carries no secret either (hq ADR 0202). +must see, a node that joins later included — kept, so it carries no secret either (hq ADR 0201). | what | how | |---|---| diff --git a/modules/claude-code/node.ts b/modules/claude-code/node.ts index af22d7f..98c8b59 100644 --- a/modules/claude-code/node.ts +++ b/modules/claude-code/node.ts @@ -9,7 +9,7 @@ // - a login a person made here — a refresh token this module never writes — is offered to the seat at // once, sealed to the seat's key: the one moment a refresh token travels, because the login made the // manager's stale; -// - an MCP server registered through this module is **state, not an event** (novox/hq ADR 0202): one +// - an MCP server registered through this module is **state, not an event** (novox/hq ADR 0201): one // key per server in the module's `servers` bucket — `all.` for every node, `.` // for one — which every node watches. A node that joins later, or was off, reads the whole current set // at start; unregistering is a delete. A secret never goes in an entry: the runtime refuses one. diff --git a/modules/claude-code/tools/index.ts b/modules/claude-code/tools/index.ts index af4bffb..e6a4224 100644 --- a/modules/claude-code/tools/index.ts +++ b/modules/claude-code/tools/index.ts @@ -5,7 +5,7 @@ // // At start it renders the agent's managed directory, asks the licence manager for this node's token, // begins watching the credentials file for a login, takes the manager's licence events, and watches the -// module's `servers` state — every node's MCP server registrations (novox/hq ADR 0202). node.ts holds the +// module's `servers` state — every node's MCP server registrations (novox/hq ADR 0201). node.ts holds the // logic. import { readFileSync, watchFile } from "node:fs"; @@ -92,7 +92,7 @@ function status(p: Paths): Record { }; } -/** The module's MCP servers on the bus (ADR 0202): its own state, which every node of it watches. */ +/** The module's MCP servers on the bus (ADR 0201): its own state, which every node of it watches. */ const servers = () => state>("servers") as unknown as ServerState; /** What this node takes from that state, kept from the watch. One per process. */ @@ -180,20 +180,29 @@ if (p) { say(JSON.stringify(await pull(p, ask, writeManaged).catch((e) => ({ failed: String(e) })))); }).catch(loud("the licence events")); - // Every node's MCP servers: the whole current set first, then each change (ADR 0202). Awaited, so the - // managed directory holds every server that applies here before the bundle says what it serves. - try { - await state>("servers").watch((c) => { + // Every node's MCP servers: the whole current set first, then each change (ADR 0201). **Not awaited + // where the module is imported**: the runtime waits on the handshake, and a bucket that is not on the + // bus yet — or a grant the bus has not reloaded — answers late; awaited here, that left the bundle + // unable to answer `initialize` in time and the module unserved (found on its first assignment). So it + // watches beside the handshake and asks again until the state answers; until then the managed + // directory holds what the file kept from the last run. + const watchServers = (attempt = 0): void => { + state>("servers").watch((c) => { try { const done = onServerChange(viewOf(p), c as ServerChange, p, writeManaged); if (done) say(done); } catch (err) { loud(`taking ${c.op} ${c.key}`)(err); // the view took it; the next render writes it } - }); - } catch (err) { - loud("watching the MCP servers")(err); - } + }).then( + () => say(`watching the MCP servers${attempt ? ` (after ${attempt} refusal(s))` : ""}`), + (err) => { + const wait = [2, 5, 10, 30][attempt] ?? 60; + say(`the MCP servers cannot be watched yet (${err instanceof Error ? err.message : String(err)}); asking again in ${wait}s`); + setTimeout(() => watchServers(attempt + 1), wait * 1000); + }); + }; + watchServers(); // Catch up once at start: a node that was off takes its current token now. void pull(p, ask, writeManaged).then((r) => say(`at start: ${JSON.stringify(r)}`), loud("asking for this node's token at start"));