Migrate enrichment to Dench gateway (dench_enrich, hybrid async)#277
Migrate enrichment to Dench gateway (dench_enrich, hybrid async)#277vedworks wants to merge 9 commits into
Conversation
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.
There was a problem hiding this comment.
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.
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.
- 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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Timeout poll marks pending incorrectly
- Removed the incorrect
pending: truefrom the timeout fallback return inpollEnrichmentJobWithTimeout, so only genuinely pending jobs (last attempt with status 'pending') are flagged as pending.
- Removed the incorrect
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 }; |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 701b78e. Configure here.



Summary
apollo_enrich→dench_enrichwith hybrid async behavior: a cache hit returns the person inline, while queued jobs return anenrichmentId+ poll path.pending/errorevents; email-only person enrichment is rejected (LinkedIn URL required).extensions/shared/dench-gateway-client.ts) mirrored intoapps/web/lib/dench-gateway-enrichment.tsto avoid a cross-workspace import the Turbopack bundler can't resolve.dench-identityprompt/skills, rebuilds plugin.mjsbundles, and addsactivation+contracts.toolsto plugin manifests so the gateway loads and registers the tools.requiredFields→enrichFields).Notes / Findings
dench_enrichworks end-to-end and reaches the gateway, but enrichment calls return402 insufficient_credits(requiredCents: 5, availableCents: 0) fromgateway.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/webenrich route tests pass (LinkedIn person, queued-job polling, pending state, company search, email rejection, entryIds narrowing/validation).enrichment-columnstests pass forgetEnrichFieldsForApolloPath.extensions/apollo-enrichmentplugin tests pass fordench_enrich..mjsbundles rebuilt; gateway loadsapollo-enrichment,exa-search,posthog-analyticsand registers their tools.dench_enrichavailable (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 toenrichFields, polls queued LinkedIn jobs with a short timeout, and emitspendingSSE 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/searchby domain rather than GET withrequiredFieldsquery params.A new
apps/web/lib/dench-gateway-enrichment.tsmirrors the extension shared client so Next.js/Turbopack does not cross the workspace boundary. Theapollo_enrichagent tool becomesdench_enrichwith actionsjob_status, hybrid sync (inline person vsenrichmentId), andenrichFieldsinstead of default ApollorequiredFieldslists; plugin manifests gainactivation.onStartupandcontracts.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-screentoh-dvh/h-full min-h-0so 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.