Skip to content

fix(memory-graph): stop hit-testing against stale nodes after resize or theme change#1284

Open
abhay-codes07 wants to merge 1 commit into
supermemoryai:mainfrom
abhay-codes07:fix/graph-stale-spatial-index
Open

fix(memory-graph): stop hit-testing against stale nodes after resize or theme change#1284
abhay-codes07 wants to merge 1 commit into
supermemoryai:mainfrom
abhay-codes07:fix/graph-stale-spatial-index

Conversation

@abhay-codes07

Copy link
Copy Markdown
Contributor

What

The first drag after any window resize or light/dark theme switch grabs a stale node object: the visible node ignores the cursor, and because drag-start reheats the simulation, it drifts off on its own.

Why: SpatialIndex.rebuild() early-returns when its content hash is unchanged, and the hash only covers node ids and rounded positions. useGraphData produces a brand-new generation of node objects whenever its memo deps change — which includes canvasWidth/Height (resize) and colors (theme switch) — with identical ids and coordinates. The [nodes] effect in GraphCanvas called rebuild(), the hash matched, and the grid kept serving the previous generation while the renderer and nodeMap moved on. InputHandler.onMouseDown then pins fx/fy on an object the renderer no longer draws. Hover and click still work (they only read node.id), which is exactly what kept this hidden.

Fix

  • rebuild() gains a force parameter; the identity-keyed [nodes] effect passes it. The per-frame rebuilds in the rAF loop keep the cheap hash guard unchanged.
  • Also removed the colors re-merge in MemoryGraph: useGraphTheme already returns the overrides merged and referentially stable by value, and the re-merge — keyed on the raw prop identity — made colors a fresh object on every render for inline colors={{...}} consumers. That rebuilt the whole node array each render and multiplied how often the stale-index window occurred.

Testing

  • New SpatialIndex tests pin both sides: the unforced hash guard keeps the old generation for identical ids/positions (documenting exactly why force exists), a forced rebuild swaps the grid to the new objects, and a forced rebuild still refreshes the hash for subsequent unforced calls
  • bun run test in packages/memory-graph — 192 pass
  • bun run check-types — clean

cc @MaheshtheDev

…ay regenerates

SpatialIndex.rebuild() early-returns on an unchanged content hash, and
the hash only covers node ids and rounded positions. useGraphData
produces a brand-new generation of node objects whenever its memo deps
change — including on container resize and light/dark theme switches —
with identical ids and coordinates. The GraphCanvas [nodes] effect then
called rebuild(), the hash matched, and the grid kept serving the
previous generation of objects while the renderer and nodeMap moved on.

The first drag after any resize or theme toggle therefore grabbed a
detached object: onMouseDown pinned fx/fy on a node the renderer no
longer draws, the visible node ignored the cursor, and the drag-start
reheat() let it drift freely. Hover and click were unaffected (they
only use node.id), which is what kept this hidden.

Give rebuild() a force parameter and pass it from the identity-keyed
effect, keeping the cheap hash guard for the per-frame rebuilds in the
rAF loop. Covered with SpatialIndex tests that pin both behaviours:
the unforced hash guard keeps the old generation (documenting why force
exists), and a forced rebuild swaps the grid to the new objects and
refreshes the hash.

Also remove the colors re-merge in MemoryGraph: useGraphTheme already
returns the overrides merged and referentially stable by value, and
re-merging keyed on the raw prop identity made `colors` a fresh object
every render for inline `colors={{...}}` consumers — rebuilding the
entire node array each render and multiplying how often the stale-index
window occurred.
Copilot AI review requested due to automatic review settings July 13, 2026 12:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

2 participants