Skip to content

feat(map): a service belongs to whoever owns it, and folds with their cluster - #582

Merged
angel-manuel merged 2 commits into
devfrom
feat/live-map-service-ownership-containers
Aug 28, 2026
Merged

feat(map): a service belongs to whoever owns it, and folds with their cluster#582
angel-manuel merged 2 commits into
devfrom
feat/live-map-service-ownership-containers

Conversation

@angel-manuel

Copy link
Copy Markdown
Contributor

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. POST /v1/services defaults to
user_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: rootOf gains a service branch, and every consumer of rootOf
membership, the +N count, 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 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 — node ids are now owner-qualified.

That leaves the event stream, which carries a bare name and an actor:
CallActivity is built from req.service before the instance is resolved, so
the owner is not on the payload to read. Graph.serviceIdFor picks the ball the
way the gateway picks the instance instead, mirroring
resolve_service_instance_by_name — the caller's own user-level instance shadows
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: 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.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. 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 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.

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 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, because the ball a packet lands on is
chosen from the actor.

seedService grew userLevel, groups and bearer passthrough, and both seed
loops now report a failure instead of swallowing it. The first version of this
fixture silently created nothing and the screenshots looked plausible anyway.

Verification

  • 17 new pure-logic tests in 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, serviceIdFor
    shadowing and fallbacks, the extra dropped once a refetch lists it, the
    spring/target agreement, and the label rules.
  • Real-stack screenshots via make e2e-up + node dashboard/scripts/screenshot-live-map.mjs.
  • svelte-check clean; vet --agentic clean.

Screenshots in a follow-up comment.

… 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.
@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
overslash Ready Ready Preview Aug 27, 2026 7:51pm

Request Review

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Code diff size

+313 / −54 across 4 files (net +259)

Area Files Added Removed Net
dashboard 4 +313 −54 +259

Source files under src/ only (.cjs, .go, .js, .jsx, .mjs, .py, .rs, .svelte, .ts, .tsx); test files and inline #[cfg(test)] modules excluded. 5 other changed files not counted.

@angel-manuel

Copy link
Copy Markdown
Contributor Author

Screenshots

Real stack (make e2e-up), captured by dashboard/scripts/screenshot-live-map.mjs. Three users, each owning an instance called notion — the same name, three different owners — plus org-level slack / github and the system overslash / http on the shared ring.

Traffic

Each notion sits inside its owner's container and lights up from its own cluster's agents. The ball a packet lands on is chosen from the actor, so a call naming notion from bruno's agent cannot land in ana's box.

traffic

Expanded

Subagents shown, and the shared ring is visibly outside every container: overslash, slack, http, github, all dashed.

expanded

Folded

ana +6 · 5 active — the +6 is 2 agents + 3 subagents + 1 service. Her notion goes with the cluster; the org ring does not move.

collapsed

Idle

idle

Comment thread dashboard/src/lib/components/map/graph.ts Outdated
`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.
@angel-manuel
angel-manuel merged commit 8e42767 into dev Aug 28, 2026
14 checks passed
@angel-manuel
angel-manuel deleted the feat/live-map-service-ownership-containers branch August 28, 2026 06:54
angel-manuel added a commit that referenced this pull request Aug 28, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant