You're not bloated, and most of the "circles" in the diagram are visualization artifact, not architecture problems. Each HTTP call shows up as two steps (request + response), so a normal round-trip looks like a circle even though it's one unit of work.
Three real items worth engineering attention: ai_sessions.py is becoming a god endpoint, the three chat services have a confusing boundary, and the auth token tables have no physical cleanup so they accrue rows forever. Everything else looks structurally healthy.
Each HTTP request and its response are encoded as two separate steps. So an API call architecturally goes one direction, but visually looks like a loop. Breakdown of the 44 backward-flowing edges:
| Kind | Count | Real circle? | Example |
|---|---|---|---|
| http_post / http_get response | 20 | artifact | Server returns 200 to client. Not a circle. |
| function_call return value | 8 | artifact | oauth_providers returns an OAuthProfile to the endpoint that called it. |
| state_update (hook → component/page) | 8 | idiomatic | Hook returns updated state, page re-renders. Pure React data flow. |
| redirect (OAuth provider → app) | 4 | real | Google/Microsoft sends user back to /oauth/callback. Architecturally required. |
| webhook | 1 | real | Stripe POSTs to /webhooks/stripe. External system re-enters us. |
| navigation / external_api / other | 3 | real | Page-to-page nav, Anthropic returning a response. |
After subtracting the request/response duality, the real backward edges are about 3% of steps, and every one of them is in a place where the architecture demands it (React state propagation, OAuth callbacks, webhooks).
The system mostly respects layer boundaries. endpoint → service (34x), service → external (37x), api_client → endpoint (30x) dominate the traffic. Things flow in the expected direction.
flowpilot_engine is the right kind of shared service goodTouched by 5 flows (start, respond, resolve, pause, abandon). That's a coordination kernel doing its job — high fan-in is correct for orchestration code.
Star topology, not a tangle. That's what a database is supposed to look like.
How many times each layer-pair appears across all steps. Bright cells = well-traveled paths. Empty cells = layer boundaries that aren't crossed (mostly a good sign).
| page | comp | hook | store | api_c | http | endp | serv | core | model | ext | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| page | 13 | 5 | 6 | 12 | 17 | · | · | · | · | · | 2 |
| comp | 1 | 5 | 2 | · | 1 | · | 1 | · | · | · | · |
| hook | 7 | 1 | · | · | 11 | · | · | · | · | · | · |
| store | · | · | · | 4 | 2 | · | 1 | · | · | · | 1 |
| api_client | · | · | · | · | · | 5 | 30 | · | · | · | 1 |
| endpoint | 3 | · | 9 | 2 | 4 | · | 1 | 34 | 8 | 2 | 29 |
| service | 1 | · | · | · | 2 | · | 3 | 9 | 5 | 4 | 37 |
| core | · | · | · | · | · | · | · | · | · | · | 4 |
| model | · | · | · | · | · | · | · | · | · | · | 1 |
| external | 4 | · | · | · | · | · | 1 | 1 | · | · | · |
| http_client | · | · | · | · | · | · | 5 | · | · | · | · |
Read row → column. Diagonal = same-layer transitions. Above-diagonal = "backward" (e.g. endpoint → hook = HTTP response). The strong upper-right concentration (endpoint → service → external) is the right shape.
Files appearing in the most flows. The first two (PostgreSQL, Anthropic) are expected; everything else is worth a glance.
| Flows | File | Layer | Read |
|---|---|---|---|
| 25 | external:postgres | external | Expected. The DB is the hub. |
| 10 | external:anthropic_api | external | Expected for an AI product. |
| 7 | backend/app/api/endpoints/ai_sessions.py | endpoint | God endpoint candidate. See concern below. |
| 6 | frontend/src/api/aiSessions.ts | api_client | Mirrors the god endpoint. Splits naturally if backend splits. |
| 5 | backend/app/services/flowpilot_engine.py | service | Healthy coordination kernel. |
| 5 | backend/app/api/endpoints/auth.py | endpoint | 5 auth flows, 5 endpoints. Reasonable. |
| 5 | frontend/src/store/authStore.ts | store | Centralized auth state. Correct. |
| 5 | frontend/src/pages/FlowPilotSessionPage.tsx | page | Worth checking — see OAuth concern. |
| 5 | frontend/src/hooks/useFlowPilotSession.ts | hook | Always co-travels with the page. Right pattern. |
ai_sessions.py is a god endpoint split candidateAppears in 7 flows. Houses ~12 route handlers in one file: create, respond, chat, resolve, escalate, pause, abandon, pickup, list, get, plus the /chat + /respond overload. It's the highest-coupled non-DB node.
Suggested seam:
session_lifecycle.py — create, resolve, escalate, pause, abandon, pickupsession_messaging.py — chat, respondFrontend aiSessions.ts would split along the same line. Net change: clearer ownership, no functional impact.
Three files exist with overlapping responsibilities:
backend/app/services/unified_chat_service.py — chat session handling, marker parsingbackend/app/services/assistant_chat_service.py — _call_ai infrastructure (Anthropic with caching, MCP, vision)backend/app/core/ai_chat_service.py — flow-builder chat for editors (separate domain)The PROJECT_CONTEXT.md note says assistant_chat_service was "removed except for retention settings," but the trace shows unified_chat_service.send_chat_message still calls into it for _call_ai. So the file is load-bearing infrastructure, not retention scaffolding.
Two paths forward:
assistant_chat_service.py → ai_call_utils.py (or fold the _call_ai function into core/ai_provider.py where the provider abstraction already lives).PROJECT_CONTEXT.md to match reality.Either way the confusing seam goes away.
19 steps, 4 backward edges, 3 self-loops — by far the most complex auth flow. Some complexity is unavoidable (provider redirect = 2 boundary crossings). But 3 self-loops on OAuthCallbackPage suggest the page is doing too much local state shuffling: CSRF state validation, code exchange, invite-code stash retrieval, JWT storage, navigation, welcome-banner logic.
Worth a look: move OAuth state handling into either authStore (which would centralize all auth state in one place) or a useOAuthCallback hook. The page itself should be mostly declarative.
Auth writes to refresh_tokens, password_reset_tokens, email_verification_tokens, and oauth_identities. Each table is individually justified (different lifecycles, different lookup patterns, JTI rotation for refresh) — this is not bloat in the code. But the cleanup story is missing.
Verified directly: retention_cleanup.py only sweeps AssistantChat. scheduler.py only has one other cleanup job, for AIConversation. The auth endpoint code in auth.py revokes tokens (UPDATE … SET revoked_at = now()) but never deletes them. So:
refresh_tokens — revoked rows stay forever. One row per login + one per refresh rotation.password_reset_tokens — one row per forgot-password request, no cleanup at all.email_verification_tokens — one row per signup (and per re-send), no cleanup.oauth_identities — correctly persistent; this is a permanent FK from user to provider, not a cleanup target.Suggested fix: add a daily APScheduler job in retention_cleanup.py (or a sibling) that hard-deletes rows where revoked_at < now() - INTERVAL '30 days' for refresh_tokens, and expires_at < now() - INTERVAL '7 days' for the two single-use token tables. Pattern matches the existing cleanup_expired_chats shape and the _cleanup_expired_ai_conversations job in scheduler.py.
Earlier draft of this concern pointed to retention_cleanup.py as the place to verify existing cleanup. That was wrong — no such cleanup exists. Corrected after direct check.
That's just React. useFlowPilotSession and FlowPilotSessionPage always travel together because the hook is that page's controller — they're maximally coupled by design, which is the right pattern.
"Pause & leave" comes out at 11% real work, 89% plumbing. That's correct — pause is structurally just PATCH status='paused'. There's no work to do beyond plumbing. The metric undersells simple flows.
Star topology, not a tangle. A database serving every flow is the architectural ideal.
http_post as plumbing even when it carries the actual payload. Work percentages should be read as roughly 2x the displayed value.
unverified (mostly knowledge-flywheel-created proposals). They're included in the totals but the conclusions don't depend on them.