Skip to content

Fix KG orphan rows, log volume, unverified-email MCP refusal, expired token status - #1186

Merged
helpcodeai merged 5 commits into
mainfrom
fix/review-1021-kg-logs-verify-meta
Oct 21, 2026
Merged

helpcodeai merged 5 commits into
mainfrom
fix/review-1021-kg-logs-verify-meta

Conversation

@helpcodeai

Copy link
Copy Markdown
Contributor

Summary

Four fixes from the 21 Oct cloud review, one commit each.

Changes

  • Knowledge graph orphan rows. kg_value_seen.connector_id had no foreign key, so deleted connectors left their value hashes behind (102 connectors, 57.5k rows on cloud), and correlate() kept trying to create kg_nodes for them: 21.8k FK errors a day in the Postgres log.
    • Migration 20261021100000_kg_value_seen_connector_fk deletes the orphans and adds kg_value_seen_connector_id_fkey (ON DELETE CASCADE) plus an index on connector_id, so the cascade doesn't scan the table every time a connector is deleted.
    • The other kg_* tables already had their FKs (nodes and state cascade, skills set null, edges via nodes).
    • Locking: the FK is added NOT VALID and then validated, with a second orphan delete in between to cover a race. I measured on prod, read-only: the orphan anti-join takes 0.7 s and a full sort of connector_id 3.8 s, so the index build should take about as long. Only kg_value_seen writes are blocked for those few seconds; connectors writes are blocked for about a second.
    • Service: correlate() only pairs rows of connectors that existed at the start of the run. nodeId() caches a refused (P2003) node for the rest of the pass instead of retrying it once per value. The value insert drops rows of a connector deleted mid-run instead of failing the whole batch.
  • Log retention. The request logs carried every request and response header, and the MCP SDK printed its responseMode: 'json' warning on every per-request handler (61k lines a day).
    • New pino serializers keep id, method, url (no query string), mcpMethod, userAgent, ip and statusCode (pino still adds responseTime). Only named headers are read, so no auth header can reach a line; the existing redaction stays as a backup.
    • createQuietMcpHandler logs the JSON-mode warning once per process. It intercepts only that one message, and only while the handler is being built (synchronously). Other warnings pass through.
    • The docker log rotation config is unchanged.
    • docs/operations/observability.md is updated to match.
  • Unverified email plus Claude connected. The guard's plain 403 looked like a broken connection to the client. The guard now throws McpEmailVerificationRequiredException, and a global filter answers it with a minimal MCP server:
    • the notice is the server instructions;
    • the only listed tool is anythingmcp_verify_email;
    • any tools/call returns isError with the notice.
    • The notice gives the masked address (ja***@example.com) and says to sign in at <FRONTEND_URL>/login to get a new code.
    • GET requests and subscriptions/listen still get a 403, now with the notice as a JSON-RPC error. The request is still refused before any controller runs, nothing of the workspace is read, and no email is sent from this path, so the verification-email rate is unchanged.
    • Off /mcp, the refusal is unchanged.
  • Expired Meta token stays expiring. The connector's last call was at 16:45 on 20 Oct, before the 17:00 expiry. With no later call, nothing ever re-checked the token.
    • ConnectorAuthStatusService now runs an hourly sweep (first run 2 min after boot, timers unref'd). It finds auth_status='expiring' AND token_expires_at <= now(), marks each connector needs_reauth / token_expired against the same credential ref, and the existing claim sends the single owner email.
    • BearerTokenRenewalService does the same when a call sees an already-expired token. The call still goes out, so Meta's answer has the last word.
    • Only the bearer renewal service sets expiring / token_expires_at, so OAuth2 connectors are not touched.

Deployment modes

  • KG and logging changes behave the same on Cloud and self-hosted.
  • The email verification guard only acts on Cloud (unchanged).
  • The token-expiry sweep runs in both modes. Without SMTP it records the status but sends no email, as before.

Type

  • Bug fix

Testing

  • Added/updated unit tests:
    • KG: orphan rows never paired, mid-run deletion asked once, insert filter.
    • Serializers: no credentials, size reduction measured through pino.
    • Quiet handler: one warning, console restored, other warnings pass through.
    • Email verification: masking, notice, filter over real HTTP for JSON/SSE and with the SDK client, listen/GET refusals, updated guard spec.
    • Expiry: sweep, timer, renewal service.
  • Migration tested on a throwaway Postgres 17: applied all earlier migrations, seeded 1000 orphan rows, applied the new one. Orphans gone, FK validated, cascade on connector delete works, an orphan insert is refused, the new filtered INSERT works, and prisma migrate diff against the schema is empty.

Commands, in packages/backend:

  • npx tsc --noEmit -p tsconfig.json: clean.
  • npm run lint: 0 errors, 11 warnings, none in changed files. Lint's unrelated rewrites of two spec files were reverted.
  • npm test: 729 suites passed, 4 skipped; 21,845 tests passed, 1,538 skipped.

Checklist

  • My code follows the existing code style
  • I have tested my changes locally
  • I have updated documentation (if applicable)
  • All existing tests pass (npm test)
  • This PR has a descriptive title

helpcodeai and others added 5 commits October 21, 2026 13:58
kg_value_seen.connector_id had no foreign key, so deleted connectors left
their value hashes behind and every correlate pass tried to create kg_nodes
for them (21.8k FK errors a day on cloud). The migration deletes the orphans
and adds the foreign key (ON DELETE CASCADE, NOT VALID + VALIDATE) with an
index on connector_id. The ingest now only pairs rows of connectors that
existed at the start of the run, caches a refused node per pass, and drops
value rows of a connector deleted mid-run instead of failing the batch.
Request logs carried every request and response header (about 1.2 KB per
/mcp request) and the MCP SDK printed its responseMode 'json' warning for
every per-request handler (61k lines a day), so the backend docker log only
reached back about 5.7 hours. Requests now log id, method, path without
query, MCP method, user agent and client address; responses only the
status. The JSON-mode warning is logged once per process; other console
warnings are untouched.
The guard refused /mcp for unverified cloud users with a plain 403, which
MCP clients show as a broken connection: 13 users who had connected Claude
were refused 438 times in under six hours with no explanation. The refusal
now goes through a filter that answers with a minimal MCP server: the
notice (masked address, sign-in link for a new code) as instructions and
as the only tool, and as the result of any tool call. Nothing of the
workspace is read and no email is sent, so the verification email rate is
unchanged.
A token marked expiring whose token_expires_at had passed stayed a warning
when the connector was not called again, and its owner was never told (a
meta-ads token expired on 20 Oct at 17:00 was still expiring a day later).
An hourly sweep now marks such connectors needs_reauth (token_expired)
against the same credential, which sends the single owner email, and the
bearer renewal service does the same when it sees an expired token.
@helpcodeai
helpcodeai enabled auto-merge (squash) October 21, 2026 12:14
@helpcodeai
helpcodeai merged commit 44ec463 into main Oct 21, 2026
14 checks passed
@helpcodeai
helpcodeai deleted the fix/review-1021-kg-logs-verify-meta branch October 21, 2026 12:22
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant