I'll dig into the Composio connect flow to understand why a source shows "connected" even when the OAuth handshake never actually completed. Let me explore the relevant code.
I launched an explorer to map the full connect flow. Let me also look directly at the Composio connection status logic in parallel while that runs.
Let me read the key files directly to understand the status logic.
Now I have the backend flow. The backend logic looks careful (status starts initiated, callback verifies ACTIVE before flipping to active). So the false-positive likely lives in the UI — how it interprets status. Let me look at the onboarding page and the status-consuming components.
This is the smoking gun. Let me confirm the Composio status semantics and check for a disconnect/reconcile path before I write the plan.
I have a clear picture. Let me quickly check the connections test to know what contract I must preserve, then wait for the explorer's findings to fill any gaps.
Let me read the remaining status consumers to scope the UI fix completely.
Diagnosis confirmed by both my reads and the explorer. Let me state it, then build the fix.
There are two compounding failures:
app/onboarding/page.tsx:122 does const connected = Boolean(c). Any source_connection row counts as "connected", so an initiated row (OAuth started, never completed) renders the green "Working" badge and hides the Connect button. That's exactly what you saw.initiated rows — when you bail on Composio's workspace-URL screen and hit browser-back, our /api/oauth/callback never runs. The row sits at initiated forever. Nothing ever verifies it against live Composio status or expires it, and the callback always redirects with ?connected=1 claiming success.So: a row exists → UI says connected → but the poller (status='active' only) never ingests. Permanent lie, no retry path.
A. UI truth — connected means status === 'active', not "row exists". initiated/error show their real label and keep an actionable Connect/Retry button.
B. Reconcile abandoned connections — a shared reconcileInitiated(userId) that checks each initiated row against live Composio: ACTIVE→active+load; terminal/past-TTL→error; genuinely-pending-and-recent→leave. Exposed as POST /api/connections/reconcile, called by onboarding on mount; the OAuth callback uses it too.
C. Callback honesty — redirect ?connected=1 only when something actually activated; otherwise ?pending=1.
Let me get cooking. First, isolate in a worktree.
Now let me verify enqueueLoad's signature and check the worktree path before editing.
Now let me create the shared reconcile helper.
Now the reconcile API route:
Now refactor the OAuth callback to use the shared reconcile and redirect honestly:
Now the onboarding UI — the core false-positive. Let me read the worktree copy and fix it:
Now the three onboarding edits. First, the notice handling (add pending):
Now the core fix — connected must mean active, and the button stays available for initiated/error:
Now improve SourceDots so an error connection is visually distinct (red) rather than grey-as-idle:
Now a focused unit test for the reconcile logic (the heart of the fix). Let me model the Supabase mock on the existing connections test:
Now let me run the new test and typecheck/lint to verify everything compiles.
The worktree lacks node_modules. Let me symlink the main checkout's to run tests:
All 10 pass. Now typecheck the changed files to catch any type errors:
The errors are all pre-existing and confined to @supabase/ssr auth files — none of my changed files appear. Let me confirm this is a pre-existing condition (missing dep in the symlinked node_modules), not something I introduced:
Confirmed pre-existing (identical 13 errors on main — @supabase/ssr simply isn't installed locally). My files add zero type errors. Let me run lint on my changes and the full test suite:
All 154 tests pass (30 files). Let me run lint on the changed files:
next lint prompts interactively because the worktree lacks the eslint config (an environment artifact, not a code issue). Let me verify formatting with prettier on my files instead:
Let me review the complete diff for coherence:
The diff against HEAD shows far more than my 6 files — the worktree branched from origin/main (the default "fresh" base), which is behind the local main. Let me confirm so my fix lands on top of current main, not stale code:
Odd — HEAD, main, and origin/main are all the same commit, yet the diff lists ~35 files. Let me see the actual worktree status to understand what's real vs. checkout noise:
35 tracked files show modified, including many I never touched. Let me see what changed in one I didn't touch, to tell real edits from formatting noise:
Diagnosis: a format hook ran repo-wide Prettier on my first write, reformatting 29 files I never logically touched (pure line-wrapping, e.g. calendar.ts). That's collateral noise. Let me restore those unrelated files so the worktree contains only my actual fix:
Filenames with spaces broke word-splitting. Let me use pathspec exclusions instead, which handles spaces correctly:
Clean — only my fix remains (3 modified, 3 new). Let me re-run the relevant tests to confirm nothing regressed after the restore:
All green. Let me record the key design invariant in memory (it updates the existing Composio-connect note), then summarize.
Done. Here's what I found and shipped.
When you bailed on Composio's hosted screen (the workspace-URL prompt) and hit back, your browser never went through our /api/oauth/callback — so the connection was never finalized. Yet the app showed "Working / connected" because:
app/onboarding/page.tsx did const connected = Boolean(c). A source_connection row in 'initiated' state (OAuth started, never finished) is not connected — but it rendered the green "Working" badge and hid the Connect button, so you couldn't even retry.initiated row. It sat at 'initiated' forever. Meanwhile the poller only ingests status='active' rows, so it silently did nothing — a permanent false-positive. The callback also always redirected with ?connected=1 even when nothing actually activated.connected = c?.status === 'active'. initiated/error now show their real label and keep an actionable Retry button. Sidebar dot gets red for error.lib/connectors/reconcile.ts (new) — reconcileInitiated(userId) re-checks each initiated row against live Composio: ACTIVE→active + enqueue load; terminal (FAILED/EXPIRED/...) or past a 10-min TTL → error; genuinely-pending-and-recent → left alone. No schema change (TTL keys off updated_at).POST /api/connections/reconcile (new) — onboarding calls it on mount, so an abandoned flow self-heals to a retryable state instead of lying.?connected=1 only when something actually went ACTIVE, else ?pending=1.reconcile.test.ts (new, 6 cases) covering active/pending/terminal/TTL/throw paths.@supabase/ssr errors are pre-existing — identical on main, package just isn't installed locally).Work is isolated in worktree branch worktree-fix-connect-false-positive. I haven't committed — say the word and I'll commit just these 6 files (per your 5/5 Greptile gate before any merge to main).
result: Fixed the Composio false-positive "connected" bug — UI now treats only status==='active' as connected, abandoned initiated rows reconcile against live Composio (active/error/pending) via new reconcileInitiated + /api/connections/reconcile, and the OAuth callback no longer claims success when nothing activated; 154/154 tests pass.
No — I haven't committed or pushed. The fix is sitting uncommitted in the worktree branch worktree-fix-connect-false-positive (3 modified + 3 new files). I held off because pushing is outward-facing and you hadn't asked.
Want me to commit and push? If so, note your standing 5/5 Greptile gate — that applies before merging a PR into main, not to the push itself, so I can push the branch now and you'd run the review before merge.
I'll commit the 6 files and push the branch. Let me verify the staging is exactly my fix first.
The repo-wide Prettier hook re-fired (back to 35 files). I'll restore the collateral again, then stage only my 6 files explicitly so the commit is clean and reviewable.
Now stage exactly my 6 files and commit:
Exactly the 6 files staged. Now commit and push:
Pushed. Commit 0f0e63e (6 files, +305/−38) is now on origin/worktree-fix-connect-false-positive.
Want me to open a PR? GitHub's ready for one: https://github.com/venusbhatia/zrux/pull/new/worktree-fix-connect-false-positive — I can create it with gh. Per your 5/5 Greptile gate, run the review and resolve all P1s before merging into main.
result: Committed and pushed the connect false-positive fix as 0f0e63e to origin/worktree-fix-connect-false-positive (6 files, +305/−38); ready for a PR.
Let me check what PR #23 is and its branch before doing anything.
PR #23 is this fix — its head branch is worktree-fix-connect-false-positive, the exact branch I just pushed to. So my commit already landed on it; no separate PR needed. Let me confirm #23 contains the commit:
PR #23 already contains my commit plus two newer follow-up commits I didn't make (844866c re-reconcile in-flight, e9708aa don't revert active rows on enqueue failure) — someone pushed onto this branch after me. Let me check the real order so I know whether my local is behind:
Good news: there's nothing to do. PR #23 already is this fix — it was opened on the same branch (worktree-fix-connect-false-positive) my commit went to, so my work is already in it.
Two follow-up commits landed on the branch after my push (from another session or your own edits), both building directly on my reconcile code:
844866c — re-reconcile in-flight connections, not just on mounte9708aa — don't revert active rows on enqueue failure; scope updatesMy local worktree is 2 commits behind origin as a result. I haven't touched anything, so no conflict.
So: the fix is already part of PR #23 — no new PR, no separate branch to merge in. Per your 5/5 Greptile gate, run the review on #23 and resolve all P1s before merging to main.
Want me to:
result: No action needed — my fix is already in PR #23 (same branch); two follow-up commits (844866c, e9708aa) were added on top by another session, so the PR is complete.