Skip to content

perf(graph): few deep traversals instead of 60 shallow (cut log noise further)#45

Merged
spinsirr merged 1 commit into
mainfrom
perf/graph-fewer-deep-roots
Jun 28, 2026
Merged

perf(graph): few deep traversals instead of 60 shallow (cut log noise further)#45
spinsirr merged 1 commit into
mainfrom
perf/graph-fewer-deep-roots

Conversation

@spinsirr

Copy link
Copy Markdown
Collaborator

The earlier fix still fired ~60 traverse_graph calls per build (1 shallow call per seed). traverse_graph(both, depth N) returns the whole reachable neighborhood's edges in ONE call, so traverse 8 roots at depth 5 instead → ~8 reads/build (and for a connected graph, more edges per root, not fewer). Same node set (list_pages) so isolated pages still render. ~15× fewer graph reads vs the original get_links+get_backlinks fan-out. typecheck + vitest 75 + build green.

…ll request-log noise)

The previous fix (1 traverse_graph/seed × up to 60 seeds) still wrote ~60 rows to
the request log per graph build. traverse_graph(both, depth N) already returns the
whole reachable neighborhood's edges in ONE call, so traversing a handful of
relevant roots at depth 5 (gbrain default, cap 10) covers the connected brain in
~8 reads instead of 60 — and for a connected graph it actually surfaces MORE edges
per root. Roots capped at 8; same node set (from list_pages) so isolated pages
still render. Combined with the 1h cache, the UI's graph reads drop ~15× vs the
original get_links+get_backlinks fan-out.

Co-Authored-By: Zypher Agent <zypher@corespeed.io>
@spinsirr spinsirr merged commit 8d714d9 into main Jun 28, 2026
1 check failed
spinsirr added a commit that referenced this pull request Jul 1, 2026
The graph rendered as all-isolated dots ("全散了") — every node degree 0,
uniform size, no edges — even though the brain has a dense link graph.

Root cause: #45 picks the deep-traversal roots as
`[...titles.keys()].slice(0, TRAVERSE_ROOTS)`. `titles` is filled
list_pages-first in updated_desc order, so the roots are just the 8
NEWEST pages — which are frequently freshly-created and not yet linked.
A depth-5 traversal from an unlinked page reaches nothing, and the rich
hubs (entities/haas, companies/corespeed) sit at position #9+, excluded
from the roots. Result: 0 edges, deterministically.

- Seed the traversal roots from the seed-query hits (relevance-ranked,
  round-robin across queries so each contributes its top hit), which
  reliably surface the hubs; fall back to recent pages only to fill
  TRAVERSE_ROOTS. On the live brain this restores 0 → 87 edges.
- edgeRows now reports ok/failure instead of collapsing both to []. When
  the graph ends up edgeless AND a traversal actually errored, buildGraph
  throws so /api/graph returns 502 (uncached) rather than caching a
  misleading scattered graph for the full 1h TTL.
- Add a test for the fail-loud path; update AGENTS.md graph bullet (which
  still described the pre-#43 get_links implementation).

Co-Authored-By: Zypher Agent <zypher@corespeed.io>
spinsirr added a commit that referenced this pull request Jul 7, 2026
* fix(graph): traverse from relevant hubs, not the newest pages

The graph rendered as all-isolated dots ("全散了") — every node degree 0,
uniform size, no edges — even though the brain has a dense link graph.

Root cause: #45 picks the deep-traversal roots as
`[...titles.keys()].slice(0, TRAVERSE_ROOTS)`. `titles` is filled
list_pages-first in updated_desc order, so the roots are just the 8
NEWEST pages — which are frequently freshly-created and not yet linked.
A depth-5 traversal from an unlinked page reaches nothing, and the rich
hubs (entities/haas, companies/corespeed) sit at position #9+, excluded
from the roots. Result: 0 edges, deterministically.

- Seed the traversal roots from the seed-query hits (relevance-ranked,
  round-robin across queries so each contributes its top hit), which
  reliably surface the hubs; fall back to recent pages only to fill
  TRAVERSE_ROOTS. On the live brain this restores 0 → 87 edges.
- edgeRows now reports ok/failure instead of collapsing both to []. When
  the graph ends up edgeless AND a traversal actually errored, buildGraph
  throws so /api/graph returns 502 (uncached) rather than caching a
  misleading scattered graph for the full 1h TTL.
- Add a test for the fail-loud path; update AGENTS.md graph bullet (which
  still described the pre-#43 get_links implementation).

Co-Authored-By: Zypher Agent <zypher@corespeed.io>

* chore: format admin files left unformatted by #42/#44

biome check has been failing repo-wide since the calibration/admin PRs
landed with unformatted files, so every branch inherits a red CI check.
Formatting-only; no behavior change.

Co-Authored-By: Zypher Agent <zypher@corespeed.io>

* fix(graph): close the fail-loud guard's blind spots + serve stale over 502

Review hardening on top of the hub-roots fix (multi-model review of
this PR):

- Every upstream read feeds ONE failure signal: seed queries and
  list_pages now track failures (a total outage used to cache an EMPTY
  graph as healthy for the 1h TTL; an all-seed-query failure silently
  reverted roots to recency order — the exact regression this PR
  fixes). edgeRows treats MCP isError results, non-array payloads, and
  edge-row-less arrays (schema drift) as failed reads instead of
  "healthy, zero edges".
- The edgeless guard gates on the PRE-filter edge set, so a brain
  whose every edge touches a hash-titled page doesn't 502.
- Degraded builds (some reads failed, edges survived) are served but
  NOT cached, so a partial graph can't get pinned for the full TTL.
- Rebuilds are single-flighted, and a failed rebuild serves the last
  good expired graph stale instead of a 502 — no full-pipeline retry
  storm against an already-degraded gbrain.
- The route logs the failure reason before the generic 502.
- Overview renders the link stat as "—" and TopHubs says the data is
  unavailable when the graph read failed, instead of a misleading 0.
- Hash-titled mem0 imports no longer consume traversal-root slots.
- Tests: root selection is pinned (reverting to recency-ordered roots
  fails the suite — mutation-verified), plus coverage for the
  uncached-throw contract, isError, total outage, partial failure,
  stale-serve, single-flight, round-robin order, and the route 502.

Co-Authored-By: Zypher Agent <zypher@corespeed.io>

---------

Co-authored-by: Zypher Agent <zypher@corespeed.io>
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