Skip to content

perf(server): stop calling our own API over HTTP during render, execution, and tool runs - #6660

Merged
waleedlatif1 merged 14 commits into
stagingfrom
perf/in-process-server-reads
Aug 13, 2026
Merged

perf(server): stop calling our own API over HTTP during render, execution, and tool runs#6660
waleedlatif1 merged 14 commits into
stagingfrom
perf/in-process-server-reads

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

Server-side work that was being done over HTTP against our own API now runs in-process, plus a set of query-layer correctness fixes found while auditing the same code.

Self-HTTP hops removed — each one cost a round trip through the load balancer plus a full re-authentication to re-derive identity the caller already held:

  • Router and evaluator blocks called POST /api/providers per block. They now call the provider runtime directly through a shared executeBlockProviderRequest, which reproduces the two admission checks that route owned.
  • Credential-using tool executions minted an internal JWT and POSTed it to POST /api/auth/oauth/token on every call, including retries. The route body moved to lib/oauth/token-resolution.ts; the route and the executor now run one identical authorization path. The browser path still goes over HTTP with the session cookie.
  • Copilot checkpoint revert PUT'd to /api/workflows/[id]/state, forwarding cookies so the other side could re-verify the session it had just verified. The PUT body moved to lib/workflows/persistence/save-normalized-state.ts, which owns authorization, the lock check, the row-locked write, custom-tool extraction, and the socket notification — so no surface can skip a step by going through a different door.

Payload and query work

  • The workspace file list is seeded into the document on every workspace route. It is now budgeted: over the budget it seeds nothing rather than a prefix (a truncated seed would silently hide files), and the read now stops before the share join and contract parse, so the large workspaces the budget protects pay least.
  • The two workspace-wide file reads project only the columns the mapper uses instead of select().
  • getWorkspaceWithOwner's request memoization was keyed on includeArchived, so the gates that disagree about archived visibility each got their own entry and it deduped nothing. It now reads the superset once and filters per caller.

Correctness fixes

  • A second row-cache walk still used a prefix shared with the search-results entry, whose shape has no rows — a cell edit with a search view open threw in onMutate and rejected the mutation. Both walks now use an allowlist prefix.
  • useCloudStorageConfigured combined an infinite staleTime with retry: false under the global retryOnMount: false, so one transient error disabled cloud-backed uploads for the tab's lifetime with no way to recover.
  • CloudWatch selector lists forwarded search into the fetch without it appearing in the query key, so different searches shared one cache entry.
  • Optimistic temp row ids now come from generateId().
  • Broke a new import cycle between the subscription and workspace-usage query modules by moving both key factories to hooks/queries/utils/, matching the existing convention.

Type of Change

  • Bug fix
  • Performance improvement

Testing

1,511 tests pass across the touched areas, including new coverage for the extracted modules, the seed budget, and the row-cache prefix. Verified the new tests can fail: disabling the workspace-access guard turns the router suite red, and restoring the removed HTTP hop in the revert route turns four tests red.

Four test files fail to load in this worktree on a pre-existing postcss/CSS-module resolution issue unrelated to these changes (one of them is a file this branch never touches).

type-check, biome, check:api-validation, check:react-query, check:client-boundary, and check:tool-registry-boundary all pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

The audit found this key was the workspaceFilesKeys bug waiting to recur. The
manager's record type and workspaceFileFolderSchema are two independent
declarations that agree today by coincidence; the seed had no parse, so adding a
column to one would have silently cached a shape a client fetch strips — and
three of its fields are z.coerce.date(), the exact divergence that put ISO
strings under the file-list key.

Its sibling is immune because listWorkspaceFilesWithShares parses at the data
layer. This does the same at the seed, and adds the shape-parity assertion the
key never had. Verified falsifiable: removing the parse turns it red. Doing so
also exposed the existing folder test as fixture-thin — a folder with only an
id, which the contract rightly rejects — so it now uses a real row.

Also points the credential block's fetchQuery at the exported staleTime
constant instead of restating 60 * 1000; it was a fifth producer on that key
free to drift from the four that share it.
…ling shut

Two functional bugs found auditing the query layer.

patchCachedRows walked tableKeys.rowsRoot non-exact, but rowsRoot is a prefix:
the find (search results) and write (pending writes) subtrees hang off it with
non-paged shapes, and the updater's old.pages.map threw on them. It runs inside
onMutate, so the whole cell edit rejected before reaching the server — reachable
as soon as a find entry exists, i.e. after the user searches the table once. The
sibling isDefaultOrderRowsQuery already excluded those subtrees and its docstring
claimed they "never match"; that was only true of the sibling. Both now share one
isRowListQueryKey helper so they cannot drift apart again.

useCloudStorageConfigured combined staleTime: Infinity, retry: false, and the
global retryOnMount: false on a workspace-independent key, so one transient
failure left it errored for the tab's lifetime with no way back — navigating or
switching workspace cannot change the key, and the upload path fails closed, so
cloud-backed uploads stayed disabled until a full reload. useVoiceSettings
carries the same three options and already escapes this with retryOnMount: true;
this one now matches.

Note: hooks/queries/workspace-files.test.tsx cannot load in a git worktree
(pre-existing postcss resolution failure), so CI is the first place that file
runs against this change.
The workspace row was read ~3x per workspace route and ~5x on settings, and the
same Max-tier entitlement was resolved twice on one render.

Memoization is deliberately partial. getWorkspaceWithOwner accepts a
transaction and forUpdate, and live callers use both, so only the plain
no-options read routes through the memo; a row read inside one caller's
transaction or under a lock it alone holds can never be served to a later
caller. includeArchived is part of the key so the two variants cannot alias.

Three substitutions were considered and rejected as behavior changes, not
optimizations: hostContext.ownerBilling resolves subscriptions differently from
hasWorkspaceTierAccess and exposes no Max tier, so it cannot answer the
Inbox/Sandbox gates; isOrganizationOnEnterprisePlan carries self-host
short-circuits ownerBilling has no equivalent for; and widening
WorkspaceHostContext to carry the full row would push owner and org ids onto
the wire for every viewer to save a server-side read, since that type is a
response contract rather than an internal struct.
…sing the credential gate

The CloudWatch log-group and log-stream selectors forwarded `search` into the
request as `prefix` but left it out of the query key, so every keystroke
resolved to the same fresh entry and no refetch fired. Server-side filtering
was dead: a log group outside the first page could not be reached. An audit of
all 69 selector definitions found these two and no others.

useSelectorOptions resolved `args.enabled ?? definition.enabled(...)`, so a
caller supplying its own gate replaced the definition's precondition rather
than narrowing it. useSelectorDisplayName knows nothing about credentials, so a
card holding a saved value with no credential context ran a query that could
only reject. The two are now conjoined. The detail hooks keep the override
deliberately — resolving one known id needs less context than listing, which
their TSDoc already documents.

The list-key fix has a test, proven to fail without it. The `enabled` change
has none: loading use-selector-query pulls the selector registry and emcn CSS,
which cannot resolve in a git worktree.
generateTempId used Date.now(), so two rows created in the same millisecond
shared an id and the first server response overwrote both — leaving one row
duplicated and the other's real id lost until a refetch. Now uses generateId(),
matching what the workflow mutations already do. Reachable by double-clicking
create, or by any scripted or bulk create.

Also documents the contract of fetchOAuthConnections, which reports an unknown
connection state as disconnected. No consumer reads that field today — both
read names and icons, and connection state comes from useWorkspaceCredentials —
so letting the query reject would blank the suggested-action rows and drop the
credential page to raw provider ids. The note is what stops a future consumer
branching on it silently.
Each was verified against the mutation that changes the data and the keys that
expose it, not taken on report.

- Workspace usage/credits were invalidated nowhere in the app. Six sites already
  refreshed subscriptionKeys after credits moved — post-run, post-wand, limit
  edits, upgrades, top-ups — and none touched workspace usage, so the credits
  chip and the run gate held their page-load values until a reload. Adds one
  shared invalidateWorkspaceUsage and calls it from all six.
- Knowledge-base list doc counts went stale: document upload, delete, and bulk
  delete invalidated only the detail key, though the list carries docCount.
- Plan switches that do not redirect refreshed only the host context, leaving
  subscription and credit state showing the previous plan.
- The copilot tool-event handler invalidated a raw workflowKeys.list, which
  covers only the active scope and skips the selector prefix; it now uses the
  shared invalidateWorkflowLists like the other thirteen call sites.
- scheduleKeys.byId was a strict prefix of scheduleKeys.schedule, so the two
  addressings aliased, and nothing invalidated byId. De-aliased and invalidated.

Not changed: the CSV preview key already folds in the file version and storage
key, so a content update addresses a different cache entry — version-in-key is
the mechanism there, not a missing invalidation.

Tests added for the usage and knowledge fixes, both proven to fail without them.
The other four live in files that cannot load in a git worktree (pre-existing
postcss resolution failure), so CI is where they first run.
…are the usage refresh

Two corrections from reviewing the previous commits.

patchCachedRows was fixed with a predicate naming the sibling subtrees to skip —
a denylist that rots the moment a fifth subtree is added under rowsRoot. The key
factory already separated row lists under an 'infinite' segment; it just had no
prefix accessor, so every caller reached for the parent and subtracted. Adding
infiniteRowsRoot lets the walk be an allowlist by construction and deletes the
predicate, the helper, and both docblocks explaining the subtraction.

The searched-rows view is consequently no longer patched by a cell edit and is
left to its own refetch — it holds a flat result, not pages. That is recorded on
the function rather than left to be rediscovered.

The delayed usage refresh was written out three times across two files, a
duplication the previous commit enlarged rather than introduced. It is now one
scheduleUsageRefresh beside the keys it invalidates, which also gives the bare
1000ms a name and one place to change it.
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 13, 2026 4:03pm

Request Review

@cursor

cursor Bot commented Aug 13, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches auth/credential resolution, workflow persistence, and provider execution paths—high-impact areas—but behavior is intended to match the removed HTTP routes with added tests; query fixes reduce silent UI bugs rather than changing server contracts.

Overview
In-process execution replaces round trips that re-authenticated work the caller already had: router and evaluator blocks call executeBlockProviderRequest instead of POST /api/providers; OAuth token logic lives in lib/oauth/token-resolution.ts with authorizeCredentialUseForAuth for route and executor; workflow state writes go through saveWorkflowNormalizedState (checkpoint revert and the state PUT route no longer HTTP-forward cookies).

SSR / prefetch seeds the workspace file list with a 300-file budget—above it nothing is seeded so the sidebar never treats a truncated list as complete—and file-folder prefetch parses through the route contract so hydrated shapes match client fetches.

React Query / hooks: table row optimistic updates walk infiniteRowsRoot only so cached search find entries don't break cell edits; CloudWatch selector keys include search; optimistic temp ids use generateId(); subscription and workspace-usage keys move to utils with shared invalidateWorkspaceUsage / scheduleUsageRefresh; knowledge doc deletes and uploads invalidate list docCount; schedule mutations refresh id-keyed reads; upgrade flows refresh broader billing state; useCloudStorageConfigured sets retryOnMount: true after transient errors.

Reviewed by Cursor Bugbot for commit aacc3e2. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces several server-to-self HTTP requests with shared in-process execution paths while preserving authorization, validation, persistence, and notification behavior.

  • Routes provider requests directly from router and evaluator handlers.
  • Centralizes credential authorization and token resolution for API and executor callers.
  • Extracts normalized workflow-state persistence for both checkpoint reverts and the state API.
  • Corrects query keys, invalidation scopes, optimistic identifiers, file projections, and server-prefetch behavior.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/executor/utils/provider-request.ts Introduces the shared in-process provider-request path used by evaluator and router handlers.
apps/sim/lib/oauth/token-resolution.ts Centralizes credential authorization, OAuth refresh, service-account resolution, auditing, and provider metadata.
apps/sim/lib/workflows/persistence/save-normalized-state.ts Consolidates workflow authorization, mutability checks, transactional persistence, custom-tool extraction, and realtime notification.
apps/sim/app/workspace/[workspaceId]/prefetch.ts Adds bounded workspace-file seeding that avoids caching an incomplete list.
apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts The previously reported broad any helper type has been replaced with the concrete provider-request type.

Sequence Diagram

sequenceDiagram
  participant Caller as Server-side caller
  participant Shared as Shared in-process service
  participant Auth as Authorization
  participant DB as Database
  participant External as Provider / Realtime service
  Caller->>Shared: Execute provider, resolve token, or save workflow state
  Shared->>Auth: Verify access and permissions
  Auth-->>Shared: Authorized context
  Shared->>DB: Read or persist required state
  Shared->>External: Provider request or update notification
  Shared-->>Caller: Typed result without self-HTTP hop
Loading

Reviews (3): Last reviewed commit: "improvement(perf): drop a duplicate auth..." | Re-trigger Greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 140b928. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Ran a line-by-line adversarial audit of the full diff across six independent passes (executor/provider, OAuth credentials, workflow-state persistence, prefetch + file reads, React Query, request memoization). Every hunk was classified rather than assumed neutral. Follow-up commit addresses what it found:

Fixed a perf regression this PR had introduced. listWorkspaceFilesWithShares had been changed to read files then shares sequentially, so it could skip the share read on the rare over-budget workspace. That taxed every under-budget request — the common case, and the one the budget exists to make fast. Restored to Promise.all; the share read is discarded on overflow.

Closed a cache-seeding hole. The file read swallows errors and returns [], so a transient DB failure would have seeded an authoritative empty list — the user is told the workspace has no files. The seed now passes throwOnError: true so a failure reaches the catch and the client fetches instead.

Hardened the shared provider entry. It now omits the runtime context entirely when there is no trace registry, rather than passing one carrying undefinedexecuteProviderTool reads the latter as "provenance expected but missing" and fails a tool call closed with no error text. Unreachable today (these blocks declare no tools) but a silent failure the day one does. Also widened the stream guard's type check and added a completion log, since the removed route was the only thing logging these calls.

Test coverage for claims that had none:

  • save-normalized-state.test.ts (new) pins the schema-equivalence claim — a checkpoint's deployedAt Date must parse identically to the JSON-serialized string it used to arrive as.
  • A regression test for the row-cache prefix collision. Verified it fails against the pre-fix code and passes after.
  • Evaluator now covers the two admission checks, mirroring the router.
  • Boundary tests at exactly maxRows, and archived-visibility tests pinning the JS filter against the SQL predicate it replaced.

Two unrelated gaps found and fixed: useRedeployWorkflowSchedule never invalidated the id-keyed schedule reads (same class as the reactivate fix, one function below it), and a stale mock in the execution test.

Notable behavior changes now documented rather than presented as neutral: provider errors reaching the executor now carry their status code, so a router/evaluator model failure can surface as 402/503/429 where the HTTP hop previously flattened everything to 500 — this matches how the agent block has always behaved. Checkpoint revert returns 404 instead of 401 for an archived workflow. Both are refusals either way.

The highest-stakes question — whether cache() could memoize an authorization decision across a long-running request — was settled empirically against this Next version in both dev and production builds: route handlers get no cache scope, so the executor re-checks per block exactly as before.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Ran /simplify and /cleanup across the full diff (7 parallel passes: reuse, complexity, efficiency, hooks/React, comments, and an exhaustive fundamental-change scan).

The scan's verdict: no fundamental changes. Nothing persisted, no authorization decision, no audit/analytics/socket event, and no wire contract differs by mechanism. Every behavioral difference is either an artifact of a removed transport being restored, or a deliberate cache/staleness fix.

Two more round trips removed:

  • Checkpoint revert authorized the workflow twice — once in the route, once inside the shared persistence helper. saveWorkflowNormalizedState now accepts an already-resolved authorization, cutting 2-3 sequential reads per revert without changing the decision or the route's 404-vs-denial distinction.
  • resolveCredentialToken awaited resolveOAuthAccountId and then authorizeCredentialUseForAuth, though neither consumes the other and both branches authorize with identical arguments. Now one Promise.all, on the hottest new path (every credentialed tool call in every run).

Dead code removed: the WorkflowLockedError catch in the PUT state route (now unreachable — the lock comes back as a failure result), an extraMetadata parameter with no callers, an unreachable ?? credentialId fallback, a needless schema export, and three conditional spreads that JSON.stringify already handled.

Comments trimmed ~50%. Speculative "what a future change would need" paragraphs deleted; multi-paragraph justifications cut to one sentence; the cache() boilerplate that appeared in four files reduced to one canonical statement. Six load-bearing comments were kept deliberately — each prevents a plausible edit that would reintroduce a bug: why executionContext is withheld, why the runtime context is omitted rather than passed as undefined, why the file seed is all-or-nothing, why archived visibility is filtered in JS, why the memoized row is immutable, and why infiniteRowsRoot exists.

Reconsidered and kept two changes I had been about to neutralize. Provider errors now carrying status codes, and revert returning 404 instead of 401 for an archived workflow, are unintentional in origin but correct in outcome — the old values were artifacts, not designs. The agent block has always propagated provider statuses; 401 means unauthenticated and the user is authenticated; and the platform's own authz layer already returns 404 for an archived workflow. Preserving those would have been preserving the bug.

One further difference the scan surfaced, same category: an AbortError from a provider now propagates instead of being flattened, so isRetryableBlockError correctly declines to retry an aborted call — again matching the agent block.

Hooks/React pass came back clean: no new effects, state, memos, or callbacks, and no performance-idiom violations. type-check, biome, all four audit gates, and 1,412 tests pass.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit aacc3e2. Configure here.

@waleedlatif1
waleedlatif1 merged commit 0c4e674 into staging Aug 13, 2026
21 of 23 checks passed
@waleedlatif1
waleedlatif1 deleted the perf/in-process-server-reads branch August 13, 2026 16:13
waleedlatif1 added a commit that referenced this pull request Aug 13, 2026
…s audit

check:utils forbids `JSON.parse(JSON.stringify(...))` and points at
structuredClone, which is right for a deep clone and wrong here: this test
exists to prove the schema accepts a `deployedAt` that arrived over HTTP as a
string as well as an in-process `Date`. structuredClone preserves the `Date`,
so adopting it would leave the test asserting nothing about the wire form.

Splits the serialize and the parse into two statements. The round trip stays
lossy — verified `JSON.parse(JSON.stringify(...))` yields a string where
structuredClone yields a Date — and the pattern the audit matches is gone.

Arrived from staging in #6660, so `check:audits` is red on origin/staging too,
not only 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.

1 participant