Skip to content

Migrate enrichment to Dench gateway (dench_enrich, hybrid async)#277

Open
vedworks wants to merge 9 commits into
mainfrom
feat/vedant/gateway-compatability
Open

Migrate enrichment to Dench gateway (dench_enrich, hybrid async)#277
vedworks wants to merge 9 commits into
mainfrom
feat/vedant/gateway-compatability

Conversation

@vedworks

@vedworks vedworks commented Jun 1, 2026

Copy link
Copy Markdown

Summary

  • Migrates Apollo-era enrichment routes to the FullEnrich-backed Dench gateway APIs across both the web app and the agent plugin.
  • Renames the agent tool apollo_enrichdench_enrich with hybrid async behavior: a cache hit returns the person inline, while queued jobs return an enrichmentId + poll path.
  • Web SSE enrich route now polls queued jobs with a short per-row timeout and surfaces pending / error events; email-only person enrichment is rejected (LinkedIn URL required).
  • Adds a shared gateway client (extensions/shared/dench-gateway-client.ts) mirrored into apps/web/lib/dench-gateway-enrichment.ts to avoid a cross-workspace import the Turbopack bundler can't resolve.
  • Removes dead Composio tool-router code, updates the dench-identity prompt/skills, rebuilds plugin .mjs bundles, and adds activation + contracts.tools to plugin manifests so the gateway loads and registers the tools.
  • Rewrites/extends tests (enrich route, enrichment-columns, apollo-enrichment plugin) for the new gateway contract and field mapping (requiredFieldsenrichFields).

Notes / Findings

  • During verification, dench_enrich works end-to-end and reaches the gateway, but enrichment calls return 402 insufficient_credits (requiredCents: 5, availableCents: 0) from gateway.merseoriginals.com. LLM chat works because it bills against a separate on-demand usage meter; enrichment requires a separate prepaid credit ledger that is currently empty for the org. This is a gateway-side billing/provisioning configuration, not a code bug in this repo.

Test plan

  • apps/web enrich route tests pass (LinkedIn person, queued-job polling, pending state, company search, email rejection, entryIds narrowing/validation).
  • enrichment-columns tests pass for getEnrichFieldsForApolloPath.
  • extensions/apollo-enrichment plugin tests pass for dench_enrich.
  • Plugin .mjs bundles rebuilt; gateway loads apollo-enrichment, exa-search, posthog-analytics and registers their tools.
  • Agent has dench_enrich available (no Composio fallback) once tool policy allows it.

Note

High Risk
Enrichment is a breaking contract change (LinkedIn-only people, new gateway endpoints, tool rename) across API routes, CRM column UX, and agent tools; incorrect mapping or polling could drop data or confuse agents until clients adapt.

Overview
This PR moves workspace and agent enrichment off legacy Apollo HTTP onto the Dench Cloud gateway (person contact, company search, people search, job polling). The web enrich route calls shared gateway helpers instead of raw fetch, maps column metadata to enrichFields, polls queued LinkedIn jobs with a short timeout, and emits pending SSE events when work is still in flight.

People enrichment behavior tightens: email is no longer an eligible input field or row identifier; validation rejects email-typed fields with 400, and per-row email values error without calling the gateway. Company enrichment uses POST /company/search by domain rather than GET with requiredFields query params.

A new apps/web/lib/dench-gateway-enrichment.ts mirrors the extension shared client so Next.js/Turbopack does not cross the workspace boundary. The apollo_enrich agent tool becomes dench_enrich with actions job_status, hybrid sync (inline person vs enrichmentId), and enrichFields instead of default Apollo requiredFields lists; plugin manifests gain activation.onStartup and contracts.tools. Agent prompts in dench-identity are updated accordingly, and a large block of unused Composio tool-router resolver code is removed from that plugin.

Tests for the enrich route, enrichment-columns, and the enrichment plugin are rewritten for the new contract. Layout/sidebar tweaks switch h-screen to h-dvh / h-full min-h-0 so the deprecation banner and main shell scroll correctly.

Reviewed by Cursor Bugbot for commit 701b78e. Bugbot is set up for automated code reviews on this repo. Configure here.

vedworks added 8 commits June 1, 2026 21:59
Introduce a typed client for the Dench gateway with helpers for person
contact, company search, enrichment jobs, and timed job polling, plus
field mapping between Apollo-era requiredFields and gateway enrichFields.
Migrate the enrichment agent tool to the Dench gateway client. Cache
hits return the person immediately, while queued jobs return an
enrichmentId and poll path without blocking the tool. Rewrite the tool
tests for the new gateway contract and hybrid async behavior.
Switch the SSE enrich route to the Dench gateway client, actively
polling queued jobs with a short per-row timeout and streaming
progress, pending, error, and done events. Add a web-side re-export of
the shared client, map enrichment columns to gateway enrichFields, and
update the route and column tests accordingly.
Strip the unused Composio tool-router orchestration and update the
identity prompt to reference dench_enrich, person contact requirements,
enrichFields, and the hybrid async enrichment flow.
Regenerate the compiled apollo-enrichment and dench-identity bundles
and update the plugin build script to reflect the dench_enrich tool and
identity prompt changes.
- Updated layout.tsx to enhance body styling for better flexbox behavior.
- Adjusted deprecation-banner.tsx to include shrink-0 class for consistent sizing.
- Modified workspace-sidebar.tsx to ensure full height and min-height for sidebar components.
- Changed page.tsx and workspace-content.tsx to use h-full instead of h-screen for more flexible height management.
- Updated workspace-content.tsx to maintain min-height for the main container, ensuring proper layout across different screen sizes.
- Removed email parameter handling from buildPersonContactBody in dench-gateway-enrichment.ts, apollo-enrichment/index.mjs, and dench-gateway-client.ts.
- Simplified error handling logic to ensure clarity in person contact requirements.
- Updated openclaw.plugin.json for Apollo Enrichment, Exa Search, and PostHog LLM Analytics to include activation on startup.
- Added contracts section specifying the tools available for each plugin, enhancing their configuration and functionality.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 4 issues found in the latest run.

  • ✅ Fixed: Email default breaks people enrich
    • Swapped priority in autoDetectInputField to prefer LinkedIn fields over email fields for people category, matching the gateway's LinkedIn URL requirement.
  • ✅ Fixed: Poll loop on empty success
    • Added an explicit branch in pollEnrichmentJobWithTimeout for succeeded-with-no-data that returns a clear error immediately instead of falling through to a tight retry loop.
  • ✅ Fixed: Apollo-only fields map to emails
    • Changed mapRequiredFieldsToEnrichFields to return undefined instead of defaulting to ["work_emails"] when all tokens are unmapped Apollo-only fields, letting the gateway use its full default backfill.
  • ✅ Fixed: Polls placeholder enrichment job id
    • Added a guard in callDenchGateway to fail fast with a descriptive error when enrichmentId is missing or "unknown" instead of polling a placeholder endpoint.

Create PR

Or push these changes by commenting:

@cursor push 35c4368854
Preview (35c4368854)
diff --git a/apps/web/app/api/workspace/objects/[name]/enrich/route.ts b/apps/web/app/api/workspace/objects/[name]/enrich/route.ts
--- a/apps/web/app/api/workspace/objects/[name]/enrich/route.ts
+++ b/apps/web/app/api/workspace/objects/[name]/enrich/route.ts
@@ -372,6 +372,10 @@
 				return { ok: true, payload: contact.result.person };
 			}
 
+			if (!contact.result.enrichmentId || contact.result.enrichmentId === "unknown") {
+				return { ok: false, error: "No enrichment data returned" };
+			}
+
 			const poll = await pollEnrichmentJobWithTimeout(
 				gatewayUrl,
 				apiKey,

diff --git a/apps/web/lib/dench-gateway-enrichment.ts b/apps/web/lib/dench-gateway-enrichment.ts
--- a/apps/web/lib/dench-gateway-enrichment.ts
+++ b/apps/web/lib/dench-gateway-enrichment.ts
@@ -85,7 +85,7 @@
     }
     // Apollo-only fields (fullName, headline, linkedinID, etc.) have no enrichFields equivalent.
   }
-  if (tokens.size === 0) return ["work_emails"];
+  if (tokens.size === 0) return undefined;
   return [...tokens];
 }
 
@@ -434,6 +434,12 @@
         person: normalizePersonRecord(job.people[0] as Record<string, unknown>),
       };
     }
+    if (job.status === "succeeded") {
+      return {
+        ok: false,
+        error: "Enrichment succeeded but returned no data",
+      };
+    }
     if (job.status === "pending" && attempt < maxAttempts - 1) {
       await new Promise((resolve) => setTimeout(resolve, delayMs));
       continue;

diff --git a/apps/web/lib/enrichment-columns.ts b/apps/web/lib/enrichment-columns.ts
--- a/apps/web/lib/enrichment-columns.ts
+++ b/apps/web/lib/enrichment-columns.ts
@@ -255,14 +255,14 @@
 ): FieldCandidate | null {
 	const eligibleFields = getEligibleInputFields(category, fields);
 	if (category === "people") {
+		const linkedinField = eligibleFields.find(
+			(f) => /linkedin/i.test(f.name),
+		);
+		if (linkedinField) return linkedinField;
 		const emailField = eligibleFields.find(
 			(f) => f.type === "email" || /^e[-_]?mail/i.test(f.name),
 		);
 		if (emailField) return emailField;
-		const linkedinField = eligibleFields.find(
-			(f) => /linkedin/i.test(f.name),
-		);
-		if (linkedinField) return linkedinField;
 	} else {
 		const domainField = eligibleFields.find(
 			(f) => /website|domain|^url$/i.test(f.name),

diff --git a/extensions/shared/dench-gateway-client.ts b/extensions/shared/dench-gateway-client.ts
--- a/extensions/shared/dench-gateway-client.ts
+++ b/extensions/shared/dench-gateway-client.ts
@@ -72,7 +72,7 @@
     }
     // Apollo-only fields (fullName, headline, linkedinID, etc.) have no enrichFields equivalent.
   }
-  if (tokens.size === 0) return ["work_emails"];
+  if (tokens.size === 0) return undefined;
   return [...tokens];
 }
 
@@ -422,6 +422,12 @@
         person: normalizePersonRecord(job.people[0] as Record<string, unknown>),
       };
     }
+    if (job.status === "succeeded") {
+      return {
+        ok: false,
+        error: "Enrichment succeeded but returned no data",
+      };
+    }
     if (job.status === "pending" && attempt < maxAttempts - 1) {
       await new Promise((resolve) => setTimeout(resolve, delayMs));
       continue;

You can send follow-ups to the cloud agent here.

Comment thread apps/web/lib/enrichment-columns.ts
Comment thread apps/web/lib/dench-gateway-enrichment.ts
Comment thread apps/web/lib/dench-gateway-enrichment.ts
Comment thread apps/web/lib/dench-gateway-enrichment.ts
Comment thread extensions/shared/dench-gateway-client.ts Fixed
- Prefer LinkedIn (not email) as people-enrichment input so bulk flows
  don't fail with "requires a LinkedIn URL" (autoDetect + eligibility).
- Return empty-result failure when a job succeeds with no people instead
  of looping the poller into a timeout.
- Map Apollo-only column tokens to undefined so the gateway keeps its
  default backfill instead of narrowing to work_emails.
- Surface an explicit "empty" person result kind instead of polling a
  placeholder "unknown" enrichment id.
- Replace trailing-slash regex with linear stripTrailingSlashes to fix
  CodeQL polynomial-regex (ReDoS) finding.

Synced fixes across the web mirror and shared client, updated tests, and
rebuilt the apollo-enrichment bundle.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Timeout poll marks pending incorrectly
    • Removed the incorrect pending: true from the timeout fallback return in pollEnrichmentJobWithTimeout, so only genuinely pending jobs (last attempt with status 'pending') are flagged as pending.

Create PR

Or push these changes by commenting:

@cursor push b7ba9ddd82
Preview (b7ba9ddd82)
diff --git a/apps/web/lib/dench-gateway-enrichment.ts b/apps/web/lib/dench-gateway-enrichment.ts
--- a/apps/web/lib/dench-gateway-enrichment.ts
+++ b/apps/web/lib/dench-gateway-enrichment.ts
@@ -454,5 +454,5 @@
     }
   }
 
-  return { ok: false, error: "Enrichment timed out", pending: true };
+  return { ok: false, error: "Enrichment timed out" };
 }

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 701b78e. Configure here.

}
}

return { ok: false, error: "Enrichment timed out", pending: true };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Timeout poll marks pending incorrectly

Medium Severity

When pollEnrichmentJobWithTimeout exhausts its attempts, it returns failure with message Enrichment timed out but still sets pending: true. The enrich SSE handler treats any pending result as a signal to emit a pending event with enrichmentId, so rows that timed out during polling can be shown as still in progress even though the route has already stopped waiting.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 701b78e. Configure here.

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.

2 participants