Skip to content

fix(connectors): serve catalog from a build-time manifest, stop cold-scan 503s#1013

Merged
buremba merged 3 commits into
mainfrom
feat/connector-catalog-manifest
May 21, 2026
Merged

fix(connectors): serve catalog from a build-time manifest, stop cold-scan 503s#1013
buremba merged 3 commits into
mainfrom
feat/connector-catalog-manifest

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented May 21, 2026

Two connector-reliability fixes from the same prod investigation (app.lobu.aisummaries-prod, current main).

1. Catalog picker showed "No connectors found" (the 503)

list_connector_definitions (with include_installable) built the picker catalog by compiling every bundled connector on demand — esbuild + a forked Node subprocess each, serially — the first time a cold pod served it, behind a per-pod in-memory cache.

On prod's 1-CPU, freshly-Flux-rolled, multi-replica app pods, that cold scan crawled (the two expected skip-warnings logged ~12s apart) and overran the request timeout → the manage_connections POST returned 503 → the React Query hook fell back to its empty [] default → "No connectors found". It briefly worked once a pod warmed, then broke on the next roll.

Fix: precompute the metadata once at build time into dist/connectors/.catalog-manifest.json (new build-connector-catalog-manifest.ts, chained in build:server); the runtime reads it and skips compilation for covered files. Files not covered (custom CONNECTOR_CATALOG_URIS dirs, missing/stale manifest) still compile on demand — the dynamic runtime path is preserved. CLI build vendors the manifest. Also hardens against runtime esbuild/npm-dep differences (metadata captured in the build env).

Repro (cold-cache fresh process per phase):

Path Connectors Time
Before — no manifest, on-demand compile 28 4426 ms
Manifest generation (build-time, once) 28 4278 ms
After — manifest present, no compile 28 3 ms

Real make build-packages: bundle 0 errors + manifest 31/33 in 5.7s; runtime reads it in 3 ms; CLI vendored it; server typechecks clean.

2. Orphan feed storming the logs (#1012)

Feed materialization threw Connector 'chrome.tabs' has no compiled code and no bundled source file on every CheckDueFeeds poll — a feed resolving to a connector definition + version row with neither compiled code nor a bundled source (prod connector_key='chrome.tabs', never a standalone bundled connector). The feed never progressed and the error repeated indefinitely.

Fix: route that case through the existing soft-delete-orphan path (already used when no active connector_definition exists), so the orphan feed stops appearing in CheckDueFeeds; operators recover by registering code + clearing deleted_at. Red→green integration test added (verified against ephemeral embedded Postgres: 1 passed).

Closes #1012.

Validation

make review: pi bug_free 80 / simplicity 84 / slop 12 / 0 bugs / 0 blockers. (Local integration suite couldn't run via embedded PG — its DB is named postgres, which the post-wipe safety guard rejects — so the #1012 test was verified manually with LOBU_ALLOW_DESTRUCTIVE_TEST_DB=1 against the ephemeral embedded PG; CI's integration job runs it against lobu_test.)

…scan 503s

The "Add a connection" picker compiled every bundled connector on demand
(esbuild + a forked metadata-extract subprocess each, run serially) the
first time a cold pod served `list_connector_definitions`. On prod's
1-CPU, freshly-rolled, multi-replica app pods that scan (~12s) overran the
request timeout and returned 503, so the catalog hook fell back to its
empty default and rendered "No connectors found".

Precompute the metadata once at build time into
`dist/connectors/.catalog-manifest.json` (via build:server) and read it at
runtime — 4426ms cold scan -> 3ms manifest read for the same connector set
in a fresh process. Files not covered by the manifest (custom
CONNECTOR_CATALOG_URIS dirs, a missing/stale manifest) still compile on
demand, so the dynamic runtime path is preserved. The CLI build vendors
the manifest next to its bundled connectors.

Refs #1012 (separate orphaned-feed bug seen in the same logs).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

📝 Walkthrough

Walkthrough

Adds build-time generation of a connector catalog manifest, runtime loading with fallback to on-demand extraction, vendoring the manifest into the CLI build, and soft-deletes feeds that reference connector versions without compiled or bundled code (with an integration test).

Changes

Connector Catalog Manifest System

Layer / File(s) Summary
Manifest contract and runtime loading
packages/server/src/utils/connector-catalog.ts
Exports manifest types/constants, adds readFile import, mtime-cached loader for .catalog-manifest.json, POSIX-relative path handling, directory scanner, and manifest-aware metadata resolution with on-demand fallback.
Manifest generation at build time
packages/server/src/utils/connector-catalog.ts, packages/server/scripts/build-connector-catalog-manifest.ts, packages/server/package.json
Adds generateCatalogManifest(dirPath) to precompute metadata for connector sources, a Bun build script that writes the manifest into dist/connectors, and updates build:server to run the generator after bundling.
Runtime integration
packages/server/src/utils/connector-catalog.ts
Updates listCatalogConnectorDefinitions to load the manifest and resolve each scanned connector file’s metadata via the manifest or fall back to extraction.
CLI distribution of manifest
packages/cli/scripts/build.cjs
Copies server dist/connectors/.catalog-manifest.json into CLI dist/connectors/ when both source manifest and destination directory exist so lobu run can use precomputed metadata.

Orphan Feed Handling

Layer / File(s) Summary
Soft-delete helper
packages/server/src/utils/queue-helpers.ts
Adds softDeleteOrphanFeed to set feeds.deleted_at and emit a structured warn log with identifiers and reason.
Integration into createSyncRun
packages/server/src/utils/queue-helpers.ts
Replaces inline soft-delete/log branches with softDeleteOrphanFeed; treats connector-version-without-code as an orphan (soft-delete + return null) instead of throwing.
Integration test for orphan-feed
packages/server/src/__tests__/integration/connectors/sync-run-orphan-feed.test.ts
Adds a Vitest integration test asserting createSyncRun returns null, soft-deletes the feed (deleted_at set), and no runs are created when compiled code and bundled source are absent.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • lobu-ai/lobu#739: Places connector sources under packages/server/dist/connectors, which this PR then uses to generate and load the precomputed manifest.
  • lobu-ai/lobu#828: Modifies connector source discovery and source_path resolution in connector-catalog.ts, overlapping with manifest-aware loading/generation.

Poem

🐰 I nibble through files and build a chart,

A tiny JSON keeps the picker smart.
When a feed’s a ghost with no code to send,
I tuck it away and mark its end.
Hops and builds—small joys to commend.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: implementing a build-time manifest to serve the connector catalog, which directly fixes the cold-scan 503 timeout issue described in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description comprehensively covers objectives, detailed fixes with context, performance metrics, validation steps, and linked issues.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/connector-catalog-manifest

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/server/src/utils/connector-catalog.ts (1)

46-61: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use interface for ExtractedConnectorCatalogMetadata (packages/server/src/utils/connector-catalog.ts, ~46-61)

export type ExtractedConnectorCatalogMetadata = { ... } defines an object shape; use export interface ExtractedConnectorCatalogMetadata { ... } per repo TypeScript standards.

Suggested change
-export type ExtractedConnectorCatalogMetadata = {
+export interface ExtractedConnectorCatalogMetadata {
   key: string;
   name: string;
   description: string | null;
   version: string;
   auth_schema: Record<string, unknown> | null;
   feeds_schema: Record<string, unknown> | null;
   actions_schema: Record<string, unknown> | null;
   options_schema: Record<string, unknown> | null;
   mcp_config: Record<string, unknown> | null;
   openapi_config: Record<string, unknown> | null;
   favicon_domain: string | null;
   required_capability: string | null;
   runtime: Record<string, unknown> | null;
   login_enabled: boolean;
-};
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/utils/connector-catalog.ts` around lines 46 - 61, Replace
the exported type alias ExtractedConnectorCatalogMetadata with an exported
interface of the same name and fields; keep all property names and types
identical (key, name, description, version, auth_schema, feeds_schema,
actions_schema, options_schema, mcp_config, openapi_config, favicon_domain,
required_capability, runtime, login_enabled) but change the declaration from
"export type ExtractedConnectorCatalogMetadata = { ... }" to "export interface
ExtractedConnectorCatalogMetadata { ... }" to follow the repo TypeScript
standard.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/server/scripts/build-connector-catalog-manifest.ts`:
- Around line 1-24: The script file build-connector-catalog-manifest.ts must be
moved from packages/server/scripts to packages/server/src (e.g.,
packages/server/src/build-connector-catalog-manifest.ts) so TypeScript sources
live under the package src directory; update any internal imports/relative paths
in the moved file (references to CATALOG_MANIFEST_FILENAME and
generateCatalogManifest and the connectorsDir/here constants) if their
resolution changes, and update the build:server (or equivalent) npm/bun build
task to point to the new path so the script is executed from packages/server/src
during the build step.
- Around line 26-31: The current check using existsSync(connectorsDir) logs a
warning and calls process.exit(0), which silently succeeds; change this to fail
the build by returning a non-zero exit or throwing an error instead. Update the
block around connectorsDir so that when the directory is missing you call
process.exit(1) or throw a new Error with a clear message (e.g., referencing
connectorsDir) rather than process.exit(0); ensure references to existsSync,
connectorsDir and process.exit are updated accordingly so the CI/build fails if
dist/connectors is missing.

---

Outside diff comments:
In `@packages/server/src/utils/connector-catalog.ts`:
- Around line 46-61: Replace the exported type alias
ExtractedConnectorCatalogMetadata with an exported interface of the same name
and fields; keep all property names and types identical (key, name, description,
version, auth_schema, feeds_schema, actions_schema, options_schema, mcp_config,
openapi_config, favicon_domain, required_capability, runtime, login_enabled) but
change the declaration from "export type ExtractedConnectorCatalogMetadata = {
... }" to "export interface ExtractedConnectorCatalogMetadata { ... }" to follow
the repo TypeScript standard.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03b07277-0786-4110-9804-72a34fcebac6

📥 Commits

Reviewing files that changed from the base of the PR and between 47f7e4f and aebd273.

📒 Files selected for processing (4)
  • packages/cli/scripts/build.cjs
  • packages/server/package.json
  • packages/server/scripts/build-connector-catalog-manifest.ts
  • packages/server/src/utils/connector-catalog.ts

Comment on lines +1 to +24
/**
* Build-time generator for the connector catalog manifest.
*
* Compiles every bundled connector once (esbuild + a forked metadata-extract
* subprocess each) and writes the result to dist/connectors/.catalog-manifest.json.
* The server then serves the bundled connector catalog by reading this file
* instead of recompiling every connector on demand — the cold per-pod scan that
* overran the request timeout and returned 503 on the "Add a connection" picker
* under prod's CPU limits (empty "No connectors found").
*
* Runs after build-server-bundle.mjs (which copies the sources into
* dist/connectors). Executed under `bun` so it can import the TS catalog code.
*/
import { existsSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
CATALOG_MANIFEST_FILENAME,
generateCatalogManifest,
} from '../src/utils/connector-catalog';

const here = dirname(fileURLToPath(import.meta.url));
const connectorsDir = join(here, '..', 'dist', 'connectors');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Move this TypeScript script under packages/server/src.

This new .ts source file is in packages/server/scripts, which violates the repo TS file placement rule. Move it under packages/server/src/... (and update build:server accordingly).

As per coding guidelines, **/*.ts: Place TypeScript source code in packages/*/src directory and tests in __tests__ directory within each workspace.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/scripts/build-connector-catalog-manifest.ts` around lines 1 -
24, The script file build-connector-catalog-manifest.ts must be moved from
packages/server/scripts to packages/server/src (e.g.,
packages/server/src/build-connector-catalog-manifest.ts) so TypeScript sources
live under the package src directory; update any internal imports/relative paths
in the moved file (references to CATALOG_MANIFEST_FILENAME and
generateCatalogManifest and the connectorsDir/here constants) if their
resolution changes, and update the build:server (or equivalent) npm/bun build
task to point to the new path so the script is executed from packages/server/src
during the build step.

Comment on lines +26 to +31
if (!existsSync(connectorsDir)) {
console.warn(
`[catalog-manifest] ${connectorsDir} missing; skipping (run build:server first).`
);
process.exit(0);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail the build when dist/connectors is missing.

Line 30 exits with code 0, which can hide a broken bundle step and ship without a manifest, reintroducing timeout-prone runtime scans.

Suggested change
 if (!existsSync(connectorsDir)) {
-  console.warn(
-    `[catalog-manifest] ${connectorsDir} missing; skipping (run build:server first).`
-  );
-  process.exit(0);
+  throw new Error(
+    `[catalog-manifest] ${connectorsDir} missing; expected after build-server-bundle.mjs`
+  );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/scripts/build-connector-catalog-manifest.ts` around lines 26
- 31, The current check using existsSync(connectorsDir) logs a warning and calls
process.exit(0), which silently succeeds; change this to fail the build by
returning a non-zero exit or throwing an error instead. Update the block around
connectorsDir so that when the directory is missing you call process.exit(1) or
throw a new Error with a clear message (e.g., referencing connectorsDir) rather
than process.exit(0); ensure references to existsSync, connectorsDir and
process.exit are updated accordingly so the CI/build fails if dist/connectors is
missing.

…code

Feed materialization threw "Connector '<key>' has no compiled code and no
bundled source file" on every CheckDueFeeds tick when a feed resolved to a
connector definition + version row that had neither persisted compiled code
nor a bundled source to compile on demand (prod: connector_key 'chrome.tabs',
which was never a standalone bundled connector). The feed never progressed and
the error repeated every poll.

Route that case through the same soft-delete-orphan path already used when no
active connector_definition exists, so the orphan feed stops appearing in
CheckDueFeeds (operators recover by registering code + clearing deleted_at).

Closes #1012.
@buremba
Copy link
Copy Markdown
Member Author

buremba commented May 21, 2026

bug_free 80, simplicity 84, slop 12, bugs 0, 0 blockers

[env] Integration suite refused DATABASE_URL db "postgres" as unsafe; failures are setup/test-db, not diff. Read diff/logs; typecheck/unit passed. Ran git diff --check, loaded catalog from dist manifest (31 defs incl google.gmail), and rebuilt manifest (31/33 files).

Suggested fixes

File Line Change
packages/server/src/utils/queue-helpers.ts 55 Move the createSyncRun JSDoc block from lines 55-61 to immediately above createSyncRun at line 201, or delete it here; it currently documents softDeleteOrphanFeed incorrectly.
packages/server/scripts/build-connector-catalog-manifest.ts 1 Shorten the file header to a brief generator summary; the detailed production incident rationale is already documented beside CATALOG_MANIFEST_FILENAME.
Full verdict JSON
{
  "bug_free_confidence": 80,
  "bugs": 0,
  "slop": 12,
  "simplicity": 84,
  "blockers": [],
  "change_type": "fix",
  "behavior_change_risk": "medium",
  "tests_adequate": false,
  "suggested_fixes": [
    {
      "file": "packages/server/src/utils/queue-helpers.ts",
      "line": 55,
      "change": "Move the createSyncRun JSDoc block from lines 55-61 to immediately above createSyncRun at line 201, or delete it here; it currently documents softDeleteOrphanFeed incorrectly."
    },
    {
      "file": "packages/server/scripts/build-connector-catalog-manifest.ts",
      "line": 1,
      "change": "Shorten the file header to a brief generator summary; the detailed production incident rationale is already documented beside CATALOG_MANIFEST_FILENAME."
    }
  ],
  "notes": "[env] Integration suite refused DATABASE_URL db \"postgres\" as unsafe; failures are setup/test-db, not diff. Read diff/logs; typecheck/unit passed. Ran git diff --check, loaded catalog from dist manifest (31 defs incl google.gmail), and rebuilt manifest (31/33 files).",
  "categories": {
    "src": 300,
    "tests": 58,
    "docs": 0,
    "config": 0,
    "deps": 2,
    "migrations": 0,
    "ci": 0,
    "generated": 0
  }
}

Local review gate — branch protection can require the pi-review commit status. See docs/REVIEW_SCHEMA.md.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/server/src/utils/queue-helpers.ts (1)

71-75: ⚡ Quick win

Use a named interface for the feed parameter shape.

Line 74 defines an inline object shape in a TypeScript signature; extract it to a local interface for guideline compliance.

♻️ Proposed refactor
+interface OrphanFeedRef {
+  connector_key: string;
+  organization_id: string;
+}
+
 async function softDeleteOrphanFeed(
   sql: DbClient,
   feedId: number,
-  feed: { connector_key: string; organization_id: string },
+  feed: OrphanFeedRef,
   reason: string
 ): Promise<void> {

As per coding guidelines, **/*.{ts,tsx}: Use interface for defining object shapes in TypeScript files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/utils/queue-helpers.ts` around lines 71 - 75, The inline
object type for the feed parameter in softDeleteOrphanFeed should be extracted
to a named TypeScript interface (e.g., declare interface OrphanFeed {
connector_key: string; organization_id: string }) placed near the top of the
file or above the function; then update the function signature to use that
interface (softDeleteOrphanFeed(sql: DbClient, feedId: number, feed: OrphanFeed,
reason: string)). Ensure the interface name is descriptive and follow existing
file export/visibility conventions.
packages/server/src/__tests__/integration/connectors/sync-run-orphan-feed.test.ts (1)

45-52: ⚡ Quick win

Replace inline cast object shapes with named interfaces.

Lines 45 and 52 use inline object-shape casts; define named interfaces for these row shapes.

♻️ Proposed refactor
+interface FeedIdRow {
+  id: number;
+}
+
+interface FeedDeletedAtRow {
+  deleted_at: Date | null;
+}
+
-    const feedId = Number((feed as { id: number }).id);
+    const feedId = Number((feed as FeedIdRow).id);
...
-    expect((after as { deleted_at: Date | null }).deleted_at).not.toBeNull();
+    expect((after as FeedDeletedAtRow).deleted_at).not.toBeNull();

As per coding guidelines, **/*.{ts,tsx}: Use interface for defining object shapes in TypeScript files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/server/src/__tests__/integration/connectors/sync-run-orphan-feed.test.ts`
around lines 45 - 52, Replace the inline object-shape casts with named
interfaces: define an interface (e.g., FeedRow { id: number }) and use it for
the `feed` cast where you compute `feedId` (currently `(feed as { id: number
})`), and define another interface (e.g., FeedDeletedRow { deleted_at: Date |
null }) and use it for the `after`/query result cast (currently `(after as {
deleted_at: Date | null })`); update the two casts in this test file and keep
the rest of the logic (including the `createSyncRun(feedId, {} as Env, sql)`
call and assertions) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@packages/server/src/__tests__/integration/connectors/sync-run-orphan-feed.test.ts`:
- Around line 45-52: Replace the inline object-shape casts with named
interfaces: define an interface (e.g., FeedRow { id: number }) and use it for
the `feed` cast where you compute `feedId` (currently `(feed as { id: number
})`), and define another interface (e.g., FeedDeletedRow { deleted_at: Date |
null }) and use it for the `after`/query result cast (currently `(after as {
deleted_at: Date | null })`); update the two casts in this test file and keep
the rest of the logic (including the `createSyncRun(feedId, {} as Env, sql)`
call and assertions) unchanged.

In `@packages/server/src/utils/queue-helpers.ts`:
- Around line 71-75: The inline object type for the feed parameter in
softDeleteOrphanFeed should be extracted to a named TypeScript interface (e.g.,
declare interface OrphanFeed { connector_key: string; organization_id: string })
placed near the top of the file or above the function; then update the function
signature to use that interface (softDeleteOrphanFeed(sql: DbClient, feedId:
number, feed: OrphanFeed, reason: string)). Ensure the interface name is
descriptive and follow existing file export/visibility conventions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: de3d2095-b0ba-48d8-ae81-c1e475fa9fba

📥 Commits

Reviewing files that changed from the base of the PR and between aebd273 and b08a6b1.

📒 Files selected for processing (2)
  • packages/server/src/__tests__/integration/connectors/sync-run-orphan-feed.test.ts
  • packages/server/src/utils/queue-helpers.ts

@buremba buremba enabled auto-merge (squash) May 21, 2026 03:09
@buremba buremba merged commit 4e5db74 into main May 21, 2026
19 of 20 checks passed
@buremba buremba deleted the feat/connector-catalog-manifest branch May 21, 2026 03:26
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.

Feed sync fails every poll: connector 'chrome.tabs' has no bundled source (orphaned feed)

2 participants