Skip to content

v0.6.88: mutex lock on oauth refresh, files export fix, hubspot trigger, search & replace UX, kb connectors multi-select, mcp negative cache#4717

Merged
waleedlatif1 merged 8 commits into
mainfrom
staging
May 22, 2026
Merged

v0.6.88: mutex lock on oauth refresh, files export fix, hubspot trigger, search & replace UX, kb connectors multi-select, mcp negative cache#4717
waleedlatif1 merged 8 commits into
mainfrom
staging

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

@waleedlatif1 waleedlatif1 commented May 22, 2026

waleedlatif1 and others added 7 commits May 21, 2026 17:21
…flow (#4705)

* improvement(hubspot): OAuth-native polling trigger replacing webhook flow

* feat(hubspot): property autocomplete, multi-filter, property-changed, list-membership, pipeline/owner dropdowns

* fix(hubspot): freeze cursor on failure + request full OAuth scopes

* chore(api): bump API route baseline from 749 to 753 for HubSpot selector routes

* fix(hubspot): make eventType required conditional on visibility

* improvement(hubspot): align trigger name and longDescription with poll-trigger conventions

* fix(hubspot): encodeURIComponent on search path segment for defense-in-depth

* fix(hubspot): cursor-based seed for list_membership polling

* fix(hubspot): Map-backed property snapshot + drop redundant filter parse
* improvement(search-replace): pass down to subblocks:

* fix local lowercasing bug
#4710)

* feat(mailer): add AWS SES and SMTP providers with auto-detect fallback

* fix(mailer): cast SES options to bridge duplicate @aws-sdk type identities

* fix(mailer): dedupe aws-sdk-sesv2, address review feedback

- Force a single @aws-sdk/client-sesv2 install via root package.json overrides; @types/nodemailer pulled in a nested copy whose nominal class brand made the two SDK type identities incompatible, breaking the CI build. With one install the cast disappears.
- Batch result message now reports successCount instead of sendable.length when entries are skipped, so "5 emails sent" no longer overstates delivery on partial failures.
- SMTP provider now warns when SMTP_HOST is set without SMTP_PORT, and when only one of SMTP_USER/SMTP_PASS is set — both previously silent misconfigurations.
- SMTP_SECURE schema is z.boolean() to match every other boolean in env.ts; runtime parsing is still handled by envBoolean.
- Strip the verbose TSDoc comments I had added.

* fix(mailer): exact sent counts in batch results, restore SES type cast

- mergeBatchResults: data.count and the message now report only emails that were actually delivered, not skipped-unsubscribed ones (they returned success: true and inflated the count). Empty-sendable branch distinguishes "all unsubscribed" from "mixed skip/failure" so the message stops lying when some entries fail validation.
- ses.ts: revert the package.json override approach (bun honors it locally but CI still installs a nested @types/nodemailer copy). Reinstate the `as unknown as` cast with a single-line WHY comment.

* fix(mailer): annotate double-cast in ses provider for strict api-validation

* fix(mailer): batch degrades isUnsubscribed errors to per-entry failures

A transient DB error in isUnsubscribed used to abort the whole batch
because the call sat outside the per-email try/catch in prepareBatch.
Wrap the unsubscribe check inside the same catch so a rejection becomes
a per-recipient failure, matching sendEmail's behavior. Lock it in
with a regression test.
…ge extraction (#4711)

* improvement(kb-connectors): multi-select fields + Slack bot/app message extraction

Adds multi-value support to KB connector configuration fields and applies it
across 8 connectors: Jira (projects), Confluence (spaces), Slack (channels),
Microsoft Teams (channels), Google Calendar (calendars), Gmail (labels),
Notion (databases), and Linear (teams + projects). Each connector emits
byte-identical externalId for legacy single-value configs so existing rows
reconcile in place via the sync engine's externalId-keyed matching.

Framework changes:
- ConnectorConfigField gains `multi?: boolean`
- New `parseMultiValue` helper in @/connectors/utils
- useConnectorConfigFields state model upgraded to string|string[]
- ConnectorSelectorField renders Combobox in multi-select mode when `field.multi`
- Add/edit connector modals handle array values end-to-end

Per-connector specifics:
- Jira: JQL `project in (...)` for 2+ keys, `project = X` for one
- Confluence: routes through CQL `space in (...)` when multi; v2 fast path stays
  for single+no-label; also fixes selector returning space.id instead of space.key
- Slack: loops per channel emitting one document each; extracts text from
  attachments and Block Kit blocks (incl. nested attachment.blocks where GitHub
  embeds PR bodies); contentHash bumped to slack-v2: to force one-time re-embed
- Microsoft Teams: loops per channel within a single team
- Google Calendar: compound cursor across calendars; single-calendar keeps
  legacy externalId/contentHash for zero-churn
- Gmail: (label:A OR label:B) with quoted-form for labels with spaces
- Notion: sequential walk via JSON compound cursor; single-DB keeps bare cursor
- Linear: GraphQL IdComparator.in for multi, eq for single

* fix(kb-connectors): valuesEqual treats legacy scalar as equal to multi-array

Existing connectors created before multi-select store sourceConfig values as
scalars (e.g. projectKey: "ENG"). With the field now declared multi: true,
resolveSourceConfig returns an array (["ENG"]), and the original valuesEqual
fell through to a strict reference comparison — falsely flagging unsaved
changes on open and triggering an unnecessary string→array shape rewrite on
save.

valuesEqual now normalizes both sides to string[] via CSV-split when either
is an array, so persisted scalar and in-memory array of the same content
compare equal. Single-value (non-multi) fields keep strict string equality.

* fix(kb-connectors): GCal externalId on config downgrade, Slack silent skip, valuesEqual order

- google-calendar getDocument: derive isMultiCalendar from the externalId's
  `:` separator instead of the current config count. Prevents duplicates when
  a user downgrades from multi to single calendar — previously the returned
  doc lost its `calendarId:` prefix and was treated as a new row by the sync
  engine, orphaning the original.
- slack listDocuments: throw on unresolvable channel instead of silently
  skipping. Matches MS Teams behaviour. Silent skip would let the sync
  engine orphan-delete the previously indexed channel content if a bot was
  removed or a channel was archived/renamed mid-life.
- edit-connector-modal valuesEqual: order-insensitive comparison for multi-
  select arrays via Set membership. Multi-select UI doesn't guarantee
  insertion order matches the server-returned order, so `["A","B"]` vs
  `["B","A"]` would otherwise flag false unsaved changes.

* chore(kb-connectors): use emptyValue() fallback in isFieldPopulated for consistency

Behavior unchanged — isValuePopulated('') and isValuePopulated([]) both return
false — but reading the field-typed fallback inline matches the convention
used elsewhere in the hook (coerceForField, handleFieldChange, resolveSourceConfig).

* fix(kb-connectors): Linear projects selector loads across all selected teams

When the team selector is in multi-select mode, the basic-mode projects
dropdown was passing only the first team ID into the linear.projects
selector context (via readFirst in resolveDepValue), so projects from other
selected teams were invisible.

resolveDepValue now joins multi-value parents into a CSV string so dependent
selectors receive every selected parent ID. The /api/tools/linear/projects
route splits the CSV teamId, fetches projects from each team in parallel,
and dedupes by project ID. Single-team configs pass through unchanged
(`split(",")` on a bare ID yields a one-element array).

The AND-of-filters semantics in buildIssuesQuery is intentional and matches
standard GraphQL filter behavior — a user filtering on teams [A,B] and
projects [X,Y] gets issues in (A or B) AND (X or Y). With this fix the
project dropdown now shows every project under any selected team so the
user can compose the right project set.

* fix(gmail-connector): always wrap OR-containing custom query, not just unwrapped ones

The previous check `!/^\(.*\)$/.test(trimmedCustom)` was supposed to avoid
double-wrapping an already-parenthesized expression, but it false-positives
on inputs like `(from:alice) OR (from:bob)` where the parens don't bracket
the whole string. Those would skip wrapping and the top-level OR would bind
across the preceding label / category / date filters instead of the custom
clause.

Always wrap when an OR is present — double-parens are a no-op in Gmail
search syntax, so `((from:a OR from:b))` parses the same as `(from:a OR
from:b)`. Simpler than walking parens depth and provably safe.
* improvement(mcp): per-server tool queries + negative cache so one slow server can't block the workspace

Move MCP tool discovery off the workspace-aggregated `Promise.all` fan-out and
onto per-server React Query keys, matching how Cursor and Claude Code render
remote MCP. `useMcpToolsQuery` is now a `useQueries` combiner: each server has
its own cache entry, its own loading state, and a slow neighbor never gates the
others. Public shape stays compatible with existing consumers.

Add a short-TTL negative cache: when `listTools` fails (timeout, connection
error, etc.) we mark the server unhealthy for 30s so subsequent discovery calls
short-circuit instead of re-paying the timeout. OAuth-required errors are
exempt so re-auth retries immediately. Drop `LIST_TOOLS_TIMEOUT_MS` from 30s
to 10s to bound the worst-case first failure.

Invalidations are per-server where the action is per-server (OAuth popup,
per-server SSE event, refresh, update, delete). Bulk operations stay
workspace-broad. Adds tests for the negative-cache behavior and the OAuth
exemption.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* improvement(mcp): in-flight dedup + 2min negative TTL + no refetch on window focus

Three small follow-ups on top of per-server tool queries:

- Coalesce concurrent `discoverServerTools(userId, serverId, workspaceId)`
  calls into a single upstream `tools/list`. Races between OAuth-callback
  cache priming, post-OAuth UI refetch, and multi-tab loads no longer
  double-fetch the same server.
- Bump negative-cache TTL from 30s to 2 minutes. Cleared on listChanged,
  OAuth completion, manual refresh, and the next successful discovery, so
  this floor only matters for genuinely dead servers — drops their floor
  traffic by 4x.
- Disable `refetchOnWindowFocus` on per-server tool queries. listChanged
  SSE + mutation invalidations already cover real schema changes; alt-tab
  no longer triggers N parallel `tools/list` calls.

* fix(mcp): address bugbot/greptile review on per-server tool discovery

- Workspace-scoped `mcpKeys.serverToolsWorkspace(workspaceId)` prefix for bulk
  invalidations (create-server, refresh-all, SSE workspace fallback, OAuth
  fallback). The previous `mcpKeys.serverTools()` prefix was global and
  invalidated every workspace's tools cache.
- `useMcpToolsQuery` folds `useMcpServers().isLoading` into the aggregate
  `isLoading` so mounting no longer flashes an "empty tools" state during the
  servers-list fetch. Aggregate `error` is suppressed when any per-server
  query already returned data so one slow server can't blank out the others.
- `useForceRefreshMcpTools` invalidates the per-server query keys of servers
  whose force refresh failed, so stale tools don't linger.
- `DiscoveryOutcome` error variant carries the original error, restoring the
  OAuth-exemption check that `getErrorMessage(...)` previously erased.
- `discoverServerTools(userId, serverId, workspaceId, forceRefresh = false)`
  now consults the positive + negative cache by default. Per-server React
  Query refetches hit the cache instead of re-paying the listTools timeout;
  callers that explicitly bypass cache (refresh route, OAuth callback, bulk
  POST refresh) pass `forceRefresh: true`. Negative-cache hits throw a typed
  `McpConnectionError` so the route layer can surface a fast 503.

* update icons

* chore(mcp): remove dead query keys and trim verbose comments

- Drop unused `mcpKeys.tools()` / `mcpKeys.toolsList()` — replaced by
  per-server keys, no remaining callers.
- Trim narrative comments to keep only the non-obvious "why" notes.

* fix(mcp): map negative-cache cooldown error to HTTP 503

`McpConnectionError` thrown when a server is in cooldown previously
fell through `categorizeError` to a generic 500. Cooldown is a
transient-unavailability condition, so route it to 503.

* test(mcp): cover cooldown error → 503 categorization

* fix(mcp): address second-round bugbot review

- useMcpToolsQuery serverIds: filter on enabled + workspaceId match.
  Disabled rows no longer trigger discover calls that get negative-cached,
  and keepPreviousData on useMcpServers no longer races a workspace switch
  into cross-workspace discover requests.
- Aggregate skips per-server data when that server's latest refetch errored,
  so a broken server's last-known tools no longer linger in the workspace
  view while its card shows an error.
- discoverServerTools failure path drops the positive cache alongside writing
  the negative-cache marker. A cache-respecting follow-up now fails fast via
  cooldown instead of returning stale tools from a now-broken server.
- useMcpTools.refreshTools drops the dead forceRefresh param — the per-server
  queryFn always sends refresh=false, so the flag was never effective. Callers
  wanting cache-bypass should use useForceRefreshMcpTools.

* chore(mcp): trim verbose comments

* fix(mcp): third-round bugbot review

- discoverTools failure path now drops the per-server positive cache alongside
  writing the negative-cache marker, matching discoverServerTools' behavior so
  a workspace-aggregate failure doesn't leave stale tools cached.
- useForceRefreshMcpTools filters disabled and out-of-workspace rows before
  fan-out so disabled servers don't 404 → negative-cache themselves.
- Remove unused useMcpServerTools export — the aggregate goes through
  useQueries directly, no external consumer exists.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 22, 2026 6:07am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 22, 2026

PR Summary

Medium Risk
Touches OAuth token refresh logic and distributed locking/negative-caching, which can impact authentication reliability under load. Also adds several new HubSpot selector endpoints and updates editor/connector UI behavior, increasing surface area but with mostly additive changes.

Overview
OAuth refresh is now coalesced to avoid thundering-herd refreshes: refresh attempts are deduped locally, coordinated via a Redis-backed leader lock, and terminal refresh failures are cached to skip repeated invalid refresh attempts.

HubSpot integration shifts to an OAuth polling model in docs and adds new API selector endpoints to fetch HubSpot owners, pipelines, properties, and lists, all using refreshAccessTokenIfNeeded + credential authorization.

Download/export responses now RFC 5987-encode filenames by reusing encodeFilenameForHeader across file export and admin workspace/folder ZIP exports to better support non-ASCII names.

UI/UX updates: knowledge-base connector config fields now support multi-select values end-to-end (state, selector field, change detection, submission), Linear project selection supports comma-separated multi-team IDs, MCP tool discovery routes can force refresh after OAuth/refresh actions, and workflow editor sub-blocks (code, conditions, checkbox list, combobox) receive improved search highlight propagation/rendering.

Reviewed by Cursor Bugbot for commit 0c96964. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0c96964. Configure here.

Comment thread apps/sim/app/api/auth/oauth/utils.ts
* fix(oauth): follower last-chance read after poll deadline

* test(oauth): exercise last-chance read in follower timeout test
@waleedlatif1 waleedlatif1 merged commit e9ee351 into main May 22, 2026
30 checks passed
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