Skip to content

feat: multi-graph follow-up — graph-scoped endpoints, MCP membership, UX, ops#166

Merged
charlie83Gs merged 7 commits intomainfrom
worktree-feat+multi-graph-endpoints
Apr 6, 2026
Merged

feat: multi-graph follow-up — graph-scoped endpoints, MCP membership, UX, ops#166
charlie83Gs merged 7 commits intomainfrom
worktree-feat+multi-graph-endpoints

Conversation

@charlie83Gs
Copy link
Copy Markdown
Contributor

Summary

Implements remaining items from #162 to make non-default graphs functional end-to-end.

Backend: Graph-Scoped Endpoints

  • 7 new graph-scoped router files for facts, edges, sources, seeds, edge-candidates, conversations, syntheses
  • Expanded graph_nodes.py with sub-resource endpoints (dimensions, facts, edges, history, convergence, perspectives, children)
  • All use GraphContext for session routing with RBAC enforcement
  • Graph-scoped synthesis creation includes graph_id in workflow dispatch
  • list_graphs now returns real node_count per graph via cross-schema queries

MCP: Per-User Graph Membership Check

  • Thread user_id through OAuth token claims (both OAuth access tokens and legacy API tokens)
  • _get_graph_factory() now verifies GraphMember role for non-default graphs — token scope check alone is no longer sufficient

Frontend: Graph Picker UX

  • GraphProvider now exposes error and switching state
  • GraphPicker shows loading spinner instead of hiding, disables during switch
  • Active graph indicator (colored dot + name) for non-default graphs
  • graphRequest() catches 404s and surfaces user-friendly message

Operational

  • Crash recovery: API startup marks provisioning-stuck graphs as error (admin can retry)
  • Qdrant auto-recovery: worker startup ensures per-graph collections ({slug}__facts, {slug}__nodes, {slug}__seeds)
  • Both operations are idempotent

Deferred

  • Graph-scoped research endpoints (complex file upload + workflow dispatch — tracked separately)
  • Integration tests for multi-graph flows (need running infra)

Closes partially #162

Test plan

  • All 55 API unit tests pass
  • All 123 frontend tests pass
  • Frontend type-check passes
  • Ruff lint + format clean
  • Integration test: create non-default graph, verify graph-scoped endpoints return data from correct schema
  • Manual test: switch graphs in UI, verify loading/error states
  • Manual test: MCP tool access denied for non-member users

🤖 Generated with Claude Code

charlie83Gs and others added 7 commits April 6, 2026 10:36
… UX, ops (#162)

Backend:
- Add graph-scoped endpoints for all major resources (nodes sub-resources,
  facts, edges, sources, seeds, edge-candidates, conversations, syntheses)
- Each resource gets a /graphs/{slug}/... router using GraphContext for
  session routing with proper RBAC checks
- Add node_count to list_graphs via per-graph cross-schema count queries
- Graph-scoped syntheses include graph_id in workflow dispatch payloads

MCP:
- Thread user_id through OAuth token claims (both OAuth access tokens
  and legacy API tokens)
- Add GraphMember role verification in _get_graph_factory for non-default
  graphs — users must be members (or superusers) to access

Frontend:
- Add error/switching state to GraphProvider context
- Show loading spinner in GraphPicker instead of hiding
- Add active graph indicator (colored dot) for non-default graphs
- Graceful 404 handling in graphRequest() with user-friendly message

Operational:
- Crash recovery: mark provisioning-stuck graphs as 'error' on API startup
- Qdrant auto-recovery: ensure per-graph collections on worker startup
- Both operations are idempotent and safe to run on every restart

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 12 research endpoints now have graph-scoped versions at
/api/v1/graphs/{slug}/research/...:

- POST prepare (file upload + chunking)
- POST {conv_id}/confirm (dispatch ingest workflow)
- GET {conv_id}/sources (list ingest sources)
- GET {conv_id}/sources/{id}/download (download file)
- POST {conv_id}/decompose (dispatch decompose workflow)
- GET {conv_id}/proposals (fetch proposed nodes)
- POST {conv_id}/build (dispatch build workflow)
- POST bottom-up/prepare (dispatch bottom-up workflow)
- GET {conv_id}/bottom-up/proposals (fetch bottom-up results)
- POST {conv_id}/agent-select (dispatch agent selection)
- GET {conv_id}/agent-select/status (poll agent status)
- GET {conv_id}/summary (fetch research summary)

All workflow dispatches include graph_id in the payload. Write
endpoints enforce require_writer access control. Helpers are
reused from research.py to avoid duplication.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
)

Code duplication (items 1 & 2):
- Extract _impl functions from all 9 handler files (conversations,
  facts, edges, sources, seeds, edge_candidates, syntheses, nodes,
  research) — 38 _impl functions total
- Graph-scoped files are now thin wrappers (~5 lines per endpoint)
  that import and call _impl functions
- Net reduction: -1302 lines (from 2457 removed, 1155 added)

N+1 in list_graphs (item 3):
- Replace sequential per-graph node count queries with
  asyncio.gather for concurrent execution

setTimeout race condition (item 4):
- Replace arbitrary setTimeout(100ms) switching state with
  switchGeneration counter that consumers can key on

MCP superuser round-trip (item 5):
- Cache is_superuser flag in token claims at token creation time
- _get_graph_factory skips membership DB query for superusers

Ambiguous path params (item 6):
- Change graph_edge_candidates pair endpoint from
  /{a:path}/{b:path} to /pair?seed_key_a=...&seed_key_b=...

Qdrant recovery log level (item 7):
- Bump from warning to error level for per-graph collection failures

Missing require_writer (item 8):
- Add require_writer to create_graph_synthesis and
  create_graph_super_synthesis

Minor — 404 masking:
- graphRequest() now only masks route-level 404s (no detail body),
  not resource-level 404s (which include "not found" in the message)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nd compat

The frontend calls /{seedKeyA}/{seedKeyB} path pattern for edge candidate
pairs. Keep graph-scoped endpoint consistent with the original to avoid
404s when graphRequest() routes to the graph-scoped version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… masking, return type

Security — MCP fail-closed:
- _get_graph_factory now raises ValueError when token has no user_id
  for non-default graphs instead of silently granting access

Bug — edge candidate path params:
- Add /pair query-param endpoint to original edge_candidates.py as
  the unambiguous variant alongside the legacy path-param route
- Keep both graph-scoped and default /{a:path}/{b:path} for backward
  compat since the frontend depends on this pattern

Fragile — 404 masking removed:
- All graph-scoped endpoints now exist, so the heuristic to distinguish
  route-level vs resource-level 404s is no longer needed
- graphRequest() now passes errors through transparently

Minor — return type annotation:
- Add FileResponse return type to download_graph_ingest_source

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merge conflict resolution:
- research.py: take our _impl-extracted version (already includes
  graph_id propagation from PR #164)
- uv.lock: regenerated

Super-synthesis graph_id propagation (item 3):
- Include graph_id in each sub_config dict so child synthesizer_wf
  dispatches target the correct graph

Stale is_superuser claim (item 4):
- Not actually stale — load_access_token and verify_token both
  re-query the User table on every request. Added clarifying comment.

WriteSessionFactory type alias (item 7):
- Replace Callable[..., "AsyncSession"] with proper
  async_sessionmaker[AsyncSession] type in edge_candidates.py

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test asserted session.get was called once, but our is_superuser
caching now calls it twice (OAuthAccessToken + User). Update the mock
to use side_effect=[row, user_mock] and assert call_count == 2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@charlie83Gs charlie83Gs merged commit 03207eb into main Apr 6, 2026
18 checks passed
@charlie83Gs charlie83Gs deleted the worktree-feat+multi-graph-endpoints branch April 6, 2026 20:08
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 6, 2026


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

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