feat: multi-graph follow-up — graph-scoped endpoints, MCP membership, UX, ops#166
Merged
charlie83Gs merged 7 commits intomainfrom Apr 6, 2026
Merged
Conversation
… 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>
|
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. |
This was referenced Apr 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements remaining items from #162 to make non-default graphs functional end-to-end.
Backend: Graph-Scoped Endpoints
graph_nodes.pywith sub-resource endpoints (dimensions, facts, edges, history, convergence, perspectives, children)GraphContextfor session routing with RBAC enforcementgraph_idin workflow dispatchlist_graphsnow returns realnode_countper graph via cross-schema queriesMCP: Per-User Graph Membership Check
user_idthrough OAuth token claims (both OAuth access tokens and legacy API tokens)_get_graph_factory()now verifiesGraphMemberrole for non-default graphs — token scope check alone is no longer sufficientFrontend: Graph Picker UX
GraphProvidernow exposeserrorandswitchingstateGraphPickershows loading spinner instead of hiding, disables during switchgraphRequest()catches 404s and surfaces user-friendly messageOperational
provisioning-stuck graphs aserror(admin can retry){slug}__facts,{slug}__nodes,{slug}__seeds)Deferred
Closes partially #162
Test plan
🤖 Generated with Claude Code