feat(map): a service belongs to whoever owns it, and folds with their cluster - #582
Conversation
… cluster The Live Map was the one surface that never read `owner_identity_id`. Ownership containers were built from `graph.rootOf`, which only knew about users, agents and subagents, so on an org whose services are mostly user-level every container came out holding identities and nothing else, and every instance sat on the shared outer ring regardless of whose it was. That ring was right for org-level instances — one of those is called from several clusters, so it belongs inside none of them — and wrong for the rest. A user-level instance is reachable only by its owner's fleet. It now joins that cluster: `rootOf` gains a service branch, and every consumer of `rootOf` — membership, the `+N` count, the fold, the box extent — picks it up for free. Instance names are unique per `(org, owner)`, not per org, and the map lists with `include_user_level=true`, so an admin's payload can hold three rows called `gcal`. Keyed by name they collapsed onto one ball, and a ball cannot sit inside three containers, so node ids are now owner-qualified. That leaves the event stream, which carries a bare name and an actor: `Graph.serviceIdFor` picks the ball the way the gateway picks the instance, the caller's own user-level one shadowing the org one. A name nothing matches stays unqualified on the ring rather than claiming an owner we cannot verify — and stays out of the dashed "org-wide" treatment for the same reason, since unknown is not absent. Placement is an offset from the owner, not a radius from the centre, and the physics springs to that same offset: a spring that disagrees with the target leaves the instance between the two and stretches the box out to reach it. `Graph.ownedServiceGap` is what they agree on, and it shrinks when the subagent rings fold away so a folded cluster is not a box of empty space. Identity labels now come from `$lib/identityDisplay`, so a container chip names its user the way the users list does — the domain-stripped email, not the IdP `name` claim, which is neither unique nor stable. The full handle survives on the tooltip, and search matches it too. The screenshot fixture seeded everything as the admin session, which after this change is a fleet whose services all pile into one container. It now seeds org-level instances explicitly (which costs a group: an org-level instance with no grant is unreachable and the API refuses it) and one `notion` per user through that user's own agent key, all under the same name — the exact shape the map has to get right. Traffic names it too, since the ball a packet lands on is chosen from the actor.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Code diff size+313 / −54 across 4 files (net +259)
Source files under |
ScreenshotsReal stack ( TrafficEach ExpandedSubagents shown, and the shared ring is visibly outside every container: Folded
Idle |
`rootOf` took whatever `resolve(owner)` returned, including an owner outside the returned identity set — archived, or filtered out. `boxRoots` then rendered a container chip for that id, labelled with the raw UUID; it stayed invisible only because `computeBoxes` finds no position for the root and drops the box, leaving the chip without `is-live`. A styling change away from a visible bug. An unknown owner is no cluster: the node keeps its `owner`, so it is still not drawn as org-wide, and the radial targets already put it on the shared ring. The same guard covers agents and subagents, where the case is older than this branch — `buildGraph` has always had a fallback for "an owner that points outside the returned set", it just did not reach `rootOf`. Found by Seer on #582.
* fix(map): a container pushes out what does not belong in it Two ways a container could tell a lie about membership. Nothing stopped a non-member from sitting inside a box. An org-level service whose ring slot happened to land over a cluster, another user's agent that swung wide, the org aggregate — any of them could come to rest inside a container, and a box is a claim about who belongs to whom. That claim got sharper in #582, when services started joining containers legitimately: "it is drawn inside the box" stopped being obviously false for a service. A stray is now pushed out by the nearest edge, caption included, since a label lying across the edge makes the same claim the ball would. The stray takes the push and the cluster takes a quarter of it back: all of it on the stray and a node held to its ring target would shove forever, all of it on the cluster and one loose ball could walk a whole fleet across the map. Container-vs-container separation existed but did not do what its comment said. It applied a flat force to every member, and the integrator divides force by mass — so a mass-1 agent accelerated ten times harder than the mass-10 user it orbits, and the cluster sheared instead of translating. It is mass-scaled now, which is what "push whole clusters" was always supposed to mean. Neither force could win against the cooling. `sp < 5 && am < 30` freezes a node outright, and at the alpha floor an overlap of a few tens of units does not clear that bar — the push was real, and then the node was frozen mid-overlap. The layout now stays awake while anything is still overlapping and settles as soon as nothing is, with a small epsilon so a residual sliver does not keep the whole map shimmering. * fix(map): a stray at its clearance is not an unresolved overlap The stray loop measures `m` against a rect already inflated by `STRAY_GAP`, so a node resting exactly at its clearance — nothing overlapping, everything where it should be — still reported `m === STRAY_GAP`, which is over the epsilon that keeps the layout above the cooling floor. One such node and the map never cools: it re-warms itself every frame, forever, for a state that is already correct. Report the penetration rather than the exit distance, the same convention the box pair above uses when it reports `mag - BOX_GAP`. The force is unchanged and was never wrong: it is proportional to the displacement needed to clear the gap, which is exactly what the inflated rect measures. Found by Seer on #584, which read the same line and diagnosed the force. * fix(map): a cluster's reaction to a stray does not thin out as it grows `pushCluster` mass-scales, so what it takes is an acceleration, and dividing that by the member count made a cluster progressively harder to shift the bigger it got — a ten-node fleet reacting at a fortieth of the stray's push rather than the quarter the line above it claims. Backwards, because a big cluster is one with a big box, so it is exactly where a stray is most likely to be stuck inside and where the reaction has work to do. The reaction exists to break the deadlock when the stray and the cluster are both held by their own ring targets; diluting it by size removed it from the case it was written for. The box-vs-box path never did this — both clusters take the same acceleration whatever their size. A flat quarter now, which is what "the cluster takes a quarter of it back" says. Found by Seer on #584. * docs(map): name what `m` measures, correctly this time The comment added with the cooling fix said a stray "resting exactly at its clearance still reports `m === STRAY_GAP`". It does not. At the clearance `m` is zero — the guard skips that case before the arithmetic runs. `m` reaches `STRAY_GAP` when the stray is *touching* the box with nothing overlapping. The code was right either way; the comment named the wrong configuration, and described a phantom force at a boundary where in fact there is none. Both places `m` is used now say which quantity they want and why they differ: the force wants the distance still needed to reach the clearance, the cooling flag wants the penetration, and touching the box is a full push for the first and nothing at all for the second. No behaviour change. --------- Co-authored-by: Factory <factory@overslash.dev>




The Live Map was the one surface that never read
owner_identity_id. Ownershipcontainers were built from
graph.rootOf, which only knew about users, agentsand subagents, so on an org whose services are mostly user-level every container
came out holding identities and nothing else, and every instance sat on the
shared outer ring regardless of whose it was.
POST /v1/servicesdefaults touser_level: true, so that is most orgs.The ring was right for org-level instances — one of those is called from several
clusters, so it belongs inside none of them — and wrong for the rest. A
user-level instance is reachable only by its owner's fleet. It now joins that
cluster:
rootOfgains a service branch, and every consumer ofrootOf—membership, the
+Ncount, the fold, the box extent — picks it up for free.Same name, different owners
Instance names are unique per
(org, owner), not per org, and the map lists withinclude_user_level=true, so an admin's payload can hold three rows calledgcal. Keyed by name they collapsed onto one ball, and a ball cannot sit insidethree containers — node ids are now owner-qualified.
That leaves the event stream, which carries a bare name and an actor:
CallActivityis built fromreq.servicebefore the instance is resolved, sothe owner is not on the payload to read.
Graph.serviceIdForpicks the ball theway the gateway picks the instance instead, mirroring
resolve_service_instance_by_name— the caller's own user-level instance shadowsthe org one. A name nothing matches stays unqualified on the ring rather than
claiming an owner we cannot verify, and stays out of the dashed "org-wide"
treatment for the same reason: unknown is not absent.
Placement
An offset from the owner, not a radius from the centre, and the physics springs
to that same offset — a spring that disagrees with the target leaves the instance
between the two and stretches the box out to reach it.
Graph.ownedServiceGapiswhat they agree on, and it shrinks when the subagent rings fold away so a folded
cluster is not a box of empty space. Org-level instances keep the shared ring and
draw dashed, with their idle edges faded, so the ring stops reading as structure.
Labels
Identity labels now come from
$lib/identityDisplay, so a container chip namesits user the way the users list does — the domain-stripped email, not the IdP
nameclaim, which is neither unique nor stable. The full handle survives on thetooltip, and search matches it too.
Fixture
The screenshot script seeded everything as the admin session, which after this
change is a fleet whose services all pile into one container. It now seeds
org-level instances explicitly — which costs a group, since an org-level instance
with no grant is unreachable and the API refuses it — and one
notionper userthrough that user's own agent key, all under the same name: the exact shape the
map has to get right. Traffic names it too, because the ball a packet lands on is
chosen from the actor.
seedServicegrewuserLevel,groupsandbearerpassthrough, and both seedloops now report a failure instead of swallowing it. The first version of this
fixture silently created nothing and the screenshots looked plausible anyway.
Verification
dashboard/tests/e2e/units/live-map-graph.spec.ts:cluster membership (including an instance owned by an agent, which roots to
that agent's user), same-name-different-owner separation,
serviceIdForshadowing and fallbacks, the extra dropped once a refetch lists it, the
spring/target agreement, and the label rules.
make e2e-up+node dashboard/scripts/screenshot-live-map.mjs.svelte-checkclean;vet --agenticclean.Screenshots in a follow-up comment.