From 4c4e9ba7a17e058dc12cb10171743329201dd7e6 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 6 Sep 2026 21:25:23 +0200 Subject: fix(node): adding a root reached node.toml but not the running node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `add_root` appended to the config and to node.toml and stopped there. `_retarget_indexer` — the MNP path — then re-points the indexer at `groups_ctx[gid]["roots"]`, an object nobody had touched, so it was retargeted at exactly what it already had. The directory was in the config file and invisible everywhere else until a restart. Worse than invisible: the ack does carry the new set, so the client showed the directory for one paint and the next index_sync took it away again — which reads as a UI bug and is not one. Adding it a second time was then refused as colliding with itself, which is the only reason anyone found out. `remove_root` and `update_root` already updated the live set; this one was missed. The loopback API hid it, because `ui/app.py` fires `reload_fn()` after the op and that re-reads node.toml from disk. The MNP path does not, and the shared-directories table only started offering Add over MNP in this refactor — a latent bug made reachable. The ack now describes the set the node will actually serve rather than one built on the side, so the two cannot disagree. test_root_ops_reach_the_live_set.py holds all three ops to it, including the counter-property that the same directory is still refused twice and that node.toml and the live set stay in step — the two halves drifting is how an operator's next restart silently undoes their last change. Four of its seven fail against the code above. Recovery on a node already in this state is `meshbay-node reload`: node.toml has everything, nothing was lost. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us --- packages/meshbay-node/src/meshbay_node/ops.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) (limited to 'packages/meshbay-node/src/meshbay_node/ops.py') diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py index 11694c3..10f6a8f 100644 --- a/packages/meshbay-node/src/meshbay_node/ops.py +++ b/packages/meshbay-node/src/meshbay_node/ops.py @@ -682,9 +682,30 @@ async def add_root(state: dict, group_id: str, path: str, *, writable=added.writable, removable=added.removable, direct=added.direct)) + # The *live* set, not only the config — the same thing `remove_root` and + # `update_root` do, and the one this was missing. + # + # `_retarget_indexer` (the MNP path) re-points the indexer at + # `groups_ctx[gid]["roots"]`, so leaving that object untouched retargeted + # it at exactly what it already had: node.toml gained the directory, the + # index went on reporting the old set, and the next `index_sync` overwrote + # whatever the ack had just told the client. The root was there, invisible, + # until a restart — and adding it again was refused as a duplicate of + # itself, which is the only reason anyone found out. + # + # Safe to append rather than rebuild: `RootSet.build(specs)` above already + # validated the whole set, this root included, for name collisions and + # nesting. + live_roots: RootSet | None = state.get("groups_ctx", {}).get( + group_id, {}).get("roots") + if live_roots is not None and not any( + r.folded == added.folded for r in live_roots.roots): + live_roots.roots.append(added) + log.info("Root added: %s → group %s", added.name, group_id[:8]) return {"status": "added", "name": added.name, "path": str(added.path), - "group_id": group_id, "roots": built.describe()} + "group_id": group_id, + "roots": (live_roots or built).describe()} async def remove_root(state: dict, group_id: str, root_name: str) -> dict: -- cgit v1.2.3