Skip to content

v0.8.6: connectors, local PDF parsing, condition block evaluation parallelization, empty states for resources - #6859

Merged
waleedlatif1 merged 14 commits into
mainfrom
staging
Aug 19, 2026
Merged

v0.8.6: connectors, local PDF parsing, condition block evaluation parallelization, empty states for resources#6859
waleedlatif1 merged 14 commits into
mainfrom
staging

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

waleedlatif1 and others added 10 commits August 18, 2026 22:15
The confirmation modal already states the consequence, and states it better:
it names the account, enumerates the workspaces that will be deleted, notes
which billing transfers instead, marks 'This cannot be undone' in the error
color, and requires typing the email to proceed. Nothing is lost by removing
the line from the row.

The wrapper it shared with the row goes too, now that the row is the section's
only child.
#6847)

The settings Back button restores a return url captured on entry, but a
workspace switch made from inside settings keeps the user in the new
workspace without touching that stored path — so Back pushed them back
into the workspace they had left, while the sidebar still read as the new
one. Discard a stored return url that names a different workspace and
fall back to the current workspace root.
…6848)

Observed in production after connectors began delivering source files: a
zero-byte PDF was stored and shipped to OCR, which answered `400 Bad Request`.
That bills an external call to discover the file was empty and reports it as an
API fault rather than as what it is. Before source files existed, an empty file
produced empty extracted text and was dropped at the empty-content check, so
this was a regression.

The emptiness rule now lives in one place, `hasIndexablePayload`, used by the
sync engine's classify and hydrate gates and by both connectors' `getDocument`.
It previously existed twice — the connectors asked whether a source file was
present while the sync engine asked the same question a second way — and a
source file with no bytes satisfied both.
* feat(knowledge): read a PDF's text layer before paying for OCR

Every PDF went to OCR, an external per-document call, even though most carry an
embedded text layer that costs nothing to read. Across a real corpus of 2,693
documents, local extraction produced text for every PDF that OCR could also read,
so the great majority of those calls bought nothing.

A PDF's text layer is now read first and used when it is good enough, leaving OCR
for the documents that actually need it. Three ways a layer fails, none of which
catches the others: there is no text at all (a scan), the text is too sparse to
be the document, or there is plenty of text that is not language — a broken
encoding, or the raw character ids a CID-keyed font emits with no ToUnicode map,
which is common in exactly the contract and procurement material that reaches a
knowledge base and which a length check alone reads as healthy.

Beyond the cost, this narrows an availability dependency: an OCR outage no longer
touches every PDF, only the minority that cannot be read locally. The threshold is
env-tunable so the balance can be moved toward cost or fidelity without a deploy.

Known limitation: the judgement is per document, so a file mixing typeset pages
with scanned inserts can average above the threshold and keep its partial text.
Per-page routing would catch it and needs per-page extraction this does not have.

The opaque-input refusal now asserts against the outbound request rather than the
storage read: local parsing is not model input, so bytes are read before the
projection is checked and still never leave the worker when it refuses.

* fix(knowledge): route a truncated PDF extraction to OCR, and drop the threshold env var

Two corrections to the text-layer triage.

A parser limit stops extraction partway and reports `truncated`. Such a result has
plenty of text by volume, so every volume-based check read it as healthy and the
document was indexed as a fragment with the remainder silently missing from
search. Truncation is now judged before anything that measures volume, and sends
the document to OCR, which reads it whole.

The characters-per-page threshold is a plain constant again. It read
`process.env` directly rather than going through the env module, and the tunable
was not worth having: a typeset page carries roughly 1,500-3,000 characters and a
scan carries none, so the value sits in a wide gap where no realistic tuning
changes an outcome. A constant is one less piece of configuration that can be set
wrong, and if the threshold is ever wrong the fix is to change it.

* fix(knowledge): take the page count from the parse that produced the text

The density check counted pages with a second, independent read of the file. The
two could disagree: a count that failed reported no pages, the check fell back to
treating the document as a single page, and a long scan carrying only a header
looked dense enough to skip OCR and be indexed as that header.

`parseBuffer` already reports the page count from the parse that produced the
text, so the two can no longer diverge, and the redundant second open of the file
goes away with it.

* fix(knowledge): chunk a long PDF for Azure OCR instead of refusing it

Both OCR providers cap how many pages a single request may carry, and both were
handling that cap differently: one split the document to fit, the other rejected
any document over it. A long PDF could therefore be ingested on one provider and
not at all on the other, for a limit that belongs to a request rather than to a
document.

The splitting, concurrency, ordering and partial-failure rule now live in one
place that both providers call, so they cannot drift apart again. A chunk that
fails is dropped rather than failing the document — losing one section of a long
document beats losing all of it — and every chunk failing still throws.

Also drops the unpdf mock from the triage tests. It was masking real behaviour:
the page count now comes from the parse metadata, so the mock was no longer
needed, and while it was in place a test asserting the old page-cap refusal
passed against both the old and new code.

* fix(knowledge): keep an unsplittable PDF and an empty OCR response honest

Two regressions from chunking the Azure path.

Splitting loads the document, which an encrypted or malformed PDF refuses, and
that failure was deciding whether the file reached OCR at all. Those are exactly
the documents the triage routes here — no readable text layer — and the provider
may well accept bytes a local parser will not, so a failed split now sends the
document whole and leaves the page cap to the provider, as it did before it was
chunked.

An Azure response carrying no pages fell back to the raw API payload as content.
Chunked, that payload counted as recovered text and was stitched into the
document; unchunked, it satisfied the empty-content check written to catch this.
No pages is now no content, so the chunk counts as failed and the document
reports it.

* fix(knowledge): fail a PDF whose OCR only partly came back

A chunked OCR run dropped any chunk that failed and returned the rest as a
normal success, so the document was marked complete with whole page ranges
absent from search and nothing downstream could tell the difference.

That contradicted the rule this change set already applies to a truncated text
layer, which is sent to OCR precisely because indexing a fragment while
reporting success is the failure being removed. A document is now indexed whole
or not at all: any missing chunk fails it, leaving it visible with a reason and
eligible for the stuck-document sweep, which can retry and produce a complete
result. Each chunk has already exhausted its own retries, so a missing one is a
real failure rather than a blip.

The page-cap test mocked fetch with a single Response object, whose body can
only be read once — the second chunk was failing on "Body already read" and the
lenient path hid it. It now returns a fresh response per call.
…d files (#6828)

* feat(resources): empty-state graphics for knowledge, tables, logs, files, skills

Four of the resource pages (knowledge, tables, logs, files) had no empty
state at all — `Resource.Table` painted column headers over a blank scroll
area and stopped there. Skills had a `: null` branch for zero data.

Adds a graphic per resource, drawn in the editor vignette's recipe: take the
product's own primitives, shrink them, strip the content to skeletons, and
let the composition bleed off the frame edges.

- Knowledge — a document fanning into the chunks it is embedded as, using the
  editor's 6px smooth-step connector language in --workflow-edge
- Tables — a sheet of cells running off two edges with one cell in an edit ring
- Logs — runs stacked newest-first, their trace spans staggered into a waterfall
- Files — a folder held open with one file still above its dashed landing slot
- Skills — a skill card opened far enough to show the tools bundled inside

`Resource.Table` gains a sanctioned `emptyState` slot rendered below the
column headers when `rows` is empty, so the chrome guarantee still holds.
Each page shows the graphic only for true zero-data — never for a search or
filter that matched nothing, never inside an empty subfolder, and (logs) never
before the first page of runs lands.

Also ports the shared `EmptyState` frame from the editor branch so this branch
stands alone, and adds a review-only /empty-states-preview gallery route.

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

* improvement(tables): redraw the empty-state graphic in the house grayscale

Matches the workflow editor's vignette and the landing feature graphics, which
between them use no brand colour at all — every one of them is built from
neutral tokens.

Two corrections:

- The blue edit ring is gone. Nothing in the reference graphics carries a hue,
  and it was the loudest element on the page.
- `--surface-4`/`--surface-5` are near-white in light mode (#f5f5f5/#f3f3f3), so
  skeleton geometry built on them dissolved on a white card. Bars now mix
  `--text-secondary` into transparent at graded strengths — a real mid-grey that
  inverts with the theme, which is the idiom the editor vignette already uses for
  the one bar it needs you to see.

Also drops the full-composition mask. The editor vignette keeps its block fully
opaque and fades only the connector strokes leaving the frame; masking
everything is what made the miniature read washed rather than deliberate. The
card is crisp now and the continuation is drawn the way a real table draws it —
an overflow fade at the edge the columns run off.

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

* improvement(tables): strip the empty-state graphic to ruled lines and a corner fade

Minimal pass. The card is gone — no border, no fill, no header shading, no type
squares. What is left is the grid itself: hairline rules in `--border-1`, ink
bars at two strengths, and the one cell held in an edit ring.

With no card fill the grid sits directly on the page, so it can dissolve into
the background instead of ending at a border. The fade is the landing page's own
idiom — two gradients intersected (`mask-composite: intersect`), crisp at the
top-left and gone through the bottom-right, the same construction
`workflow-graph-preview` uses.

Two placement notes:

- The grid is offset right of frame centre. A diagonal dissolve puts the visual
  mass toward its opaque corner, so centring the geometry would leave the
  graphic reading left of the copy beneath it.
- The selected cell sits in the quadrant the fade leaves fully opaque. A
  selection ring dissolving mid-stroke reads as a rendering fault, not a detail.

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

* improvement(tables): make the selected cell opaque and a shade darker

The ring mixed `--text-secondary` into `transparent`, so the grid rules running
underneath showed through its own stroke. Mixing into `--bg` instead holds the
same apparent value while staying opaque, and still inverts with the theme.

Raised 32% -> 46% so it reads as chrome rather than more content, and added a
stacking context: neighbouring cells are later siblings, so their rules were
painting over the ring's right and bottom edges.

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

* improvement(tables): round the empty-state grid's crisp corner

6px on the top-left only — the one corner the fade leaves intact, and the same
radius the workflow editor's vignette uses. The other three dissolve, so there
is nothing there to round.

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

* feat(knowledge,tables): redraw knowledge's empty state and add docs/create chips

Knowledge gets the same treatment tables just went through: no brand colour (the
`--brand-knowledge` accent is gone), no card chrome, ink mixed from
`--text-secondary`, and the landing page's intersected corner fade.

The graphic is a document and the chunks it is embedded as. Its fade is held
back further than the tables grid on both axes — the document has to stay whole
for the graphic to mean anything, so only the chunk grid may trail off. The
three chunks the edges actually land on are the only filled ones; filling the
whole first column left a chunk with no edge feeding it.

Both empty states now carry two chips in the frame's action slot — a docs link
and the create action, each running the same handler as the header's primary
chip and inheriting its disabled state.

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

* review(knowledge): three candidate depictions, and lead with the create chip

Chip order swapped on both empty states — the primary action reads first, the
docs link second.

Adds a review-only `knowledge-alternates.tsx` rendered in the preview gallery,
because the document-to-chunks graphic is not landing. Three directions:

- A. the embedding mesh — the landing hero's own knowledge-base panel already
  draws a base this way (`stage-kb.tsx`), so this is the house depiction rather
  than a new invention
- B. a stack of documents — the most literal reading, at the cost of colliding
  with what the files empty state wants to draw
- C. a query and the passages that answered it — depicts what a base is for,
  which is what the description copy actually promises

Delete this file and the gallery entries once one is chosen.

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

* feat(knowledge): draw the empty state as an isometric set of volumes

Replaces the document-to-chunks diagram, which read as a workflow graph rather
than as a knowledge base.

Built on the landing page's iso-illustration recipe rather than a new one:
`ISO_STROKE` contours (`--text-subtle` mixed toward `--text-muted`) at the shared
3.2 stroke width, faces filled from the three-tier surface ramp brightest-on-top,
round caps and joins. Geometry is authored in a large unit space so that 3.2
lands as a hairline once scaled to empty-state size — the same reason the landing
marks draw 3.2 into a ~526-unit viewBox.

The projection and faces are computed rather than hand-authored as path data, so
the volumes stay coherent when the geometry is retuned.

No corner fade here. The fade belongs to repeating structures that mean the same
thing cropped — the tables grid keeps its meaning with two columns or four. A
discrete object does not, which is also why the workflow editor's vignette keeps
its block fully opaque.

Drops the three candidate depictions now that the direction is settled.

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

* improvement(knowledge): brand the front volume instead of laying a page beside it

Drops the loose page on the ground and puts the knowledge-base mark on the front
volume's cover — the same `Database` glyph the sidebar and the page header use,
so the empty state names its own resource.

The mark is laid into the cover's plane rather than drawn over it. The cover is
the face at max x, spanned by the volume's depth across and its height up;
walking those two edges gives the face's basis vectors in projected space, and an
affine matrix built from them maps flat artwork into the face. So the glyph
skews with the isometric, and because both vectors derive from the box, retuning
the volumes carries the mark with them instead of stranding hand-fitted path data.

Its stroke is pre-divided by the same factor the matrix scales by, so the glyph's
contours land at the volumes' weight rather than four times it.

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

* improvement(knowledge): bore through the front volume, and fade the set back

Replaces the mark on the cover with a hole through it.

The bore is authored as a plain circle in the cover's own plane and skewed into
an ellipse by the face matrix. Its far mouth is the same circle stepped back
through the volume: boring straight back is a world step of `-w` along x, and
solving the cover-plane matrix for the local offset that produces it gives
`(+w, -w)`. The sliver of near mouth the far mouth fails to cover is exactly the
wall you see down the hole, so the depth falls out of the geometry rather than
being drawn by hand.

Down the hole the near mouth is floored in a tone darker than any outer face —
the wall turns away from the light — and the far mouth is painted in the cover
tone of the volume standing behind it, because looking through a hole in the
front volume lands on that volume's face, not on the page.

Corners stay square. Rounding was tried and reverted: rounding each face
separately notches every corner where three faces meet, and rounding the
silhouette instead cost a clip per volume for a softness the set did not want.

The tables grid's corner fade is applied along the other diagonal. There it
dissolves toward the bottom-right because a grid keeps its meaning cropped; here
the set recedes up and to the left and the front volume carries the bore, so
anchoring at the bottom-right eats into the back of the stack and reads as more
volumes behind.

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

* feat(resources): logs and files graphics, and settle the set as one collection

Logs is an activity feed — newest run lifted onto its own card, older ones
settling behind it. The relative stamps are the only literal text in any of these
graphics; everything else stays skeleton, so nothing here has to be translated or
kept true.

Files is a folder with sheets standing proud of its front panel. Depth comes from
the surface ramp rather than shadow, which would need separate light and dark
recipes where the ramp inverts on its own. The tab's diagonal is filleted at both
ends and every outer corner shares one radius — mixing radii, or running the
diagonal into square junctions, made the corners fight at this size.

Consistency pass across the set:

- Titles are the resource name alone. "No tables yet" earned nothing the
  description does not already say.
- The knowledge mark is mirrored so its bore faces left. Rebuilt on the geometry
  rather than flipped, since a flip would have put the shading on the wrong side.
  Its contours are thinned and mixed toward `--border-1`: the landing marks are
  the focal art of their section, but this one sits beside a ruled grid whose
  lines are 1px, and full-weight contours read as ink next to it.
- The logs feed is sized to the same ~148px footprint as the rest. The frame
  centres graphic and copy together, so a taller graphic pushes its title out of
  line with the others' and the set stops reading as one thing.
- Every empty state carries its create action and a docs link, each running the
  same handler as the header's primary chip.
- Fades run whichever way the subject recedes: the tables grid to the
  bottom-right, the knowledge set up and right, the logs feed down, the folder
  up.

Fixes a duplicate React key in the knowledge mark — the volumes stack along y
now, so keying on `box.x` gave every one of them `0`.

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

* revert(skills): drop the skills empty state

Removed at request. The skills list goes back to rendering nothing for zero data,
which is what it did before this branch.

Takes `vignette.tsx` with it — the shared stage and skeleton bar were left over
from the first pass, and skills was the last thing still importing them once the
other four graphics were redrawn.

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

* cleanup(resources): fix empty-state flashes and share the iso ramp

Drops the review-only preview route and gallery, which the branch always
meant to delete before merging.

Three ways the zero-data graphic painted over a workspace that has content:

- The gate read the instant URL search term while `rows` is filtered by the
  debounced one, so clearing a search that matched nothing showed the full
  "you have nothing yet" state for one debounce window.
- Nothing gated on the list still loading. Knowledge and tables hydrate from
  a server prefetch that is allowed to seed nothing, and the files list
  deliberately seeds nothing above 300 rows — so the emptiest-looking screen
  was shown to the fullest workspaces.
- The filters are part of the query key and every list keeps the previous
  key's data, so `isLoading` is false across a filter change. Only the
  placeholder gate suppresses the graphic during that refetch.

Also folds the re-declared isometric fills and stroke back onto the shared
`iso-illustration-style` source they were copied from, so a change to the iso
ramp reaches this mark too; only the stroke width still diverges. The static
face paths move to module scope, the bore interior becomes a named component
so its note is TSDoc rather than a JSX comment, and the four identical docs
chips become one.

* simplify(resources): reuse the iso recipe and let the frame own its layout

Hides the empty-state graphic behind `error` as well. A failed load also
leaves `rows` empty, and inviting someone to create their first item is the
wrong answer to a request that did not complete — all four pages only logged
the error, so the zero-data copy was what a failed load actually rendered.

`iso-illustration-style` moves out of the landing route group to
`components/iso/`. Importing it from a workspace route was the only
workspace-to-landing edge in the app, one directory away from an `iso-marks`
barrel that pulls ~10KB gzipped of illustration components — a hazard for
whoever needs the second constant. The contour recipe is now shared too:
`createIsoLineProps` takes an optional stroke width, so the knowledge mark
stops re-declaring it and only its weight diverges.

`EmptyState` owns the action row's layout, so the three pages with two chips
drop their wrapper div and every empty state's chips sit identically. Its
unused `className` prop goes with them. Also drops the `height` prop that had
one caller passing its default, and the `CORNER` constant that promised
single-sourcing the path's four bare literals did not honour.

* simplify(resources): decide list emptiness in one place

"This list holds nothing" was derived in four pages, each with the same seven
clauses under the same nine-line comment. Adding the `error` gate one commit
ago took four identical edits, and the skills empty state that was reverted
off this branch would have made it five copies.

`isResourceListEmpty` now owns the rule and the reasoning behind each gate.
Logs omits the folder argument because it has no folder navigation; the other
three pass theirs.

`Resource.Table` also wraps the slot in its own growth box, so the empty state
centres because the table says so rather than because the node handed to it
happened to carry `flex-1`.

* chore(audits): re-record the page module-graph baseline

The four empty-state graphics and the shared frame add **+10 modules** to each
of the five routes that render them — measured against `origin/staging`, not
against the recorded baseline:

    files/[fileId]  1958 -> 1968
    files           1958 -> 1968
    knowledge       2167 -> 2177
    logs            1727 -> 1737
    tables          1817 -> 1827

The baseline itself was last recorded in #6697, and staging has drifted up to
+29 on tables since — inside the max(25, 2%) tolerance on its own, but close
enough that this +10 tipped it over. So the failure was the stale baseline
meeting a small real addition, not a heavy import. The other 29 entries move
only by that accumulated drift.

The graphics stay eagerly imported on purpose: an empty state is the first
thing a new workspace paints, and deferring ~4KB gzipped behind a chunk
request would trade a shared, already-fetched module for a visible pop on the
one screen where the product has to look like it works.

* fix(resources): hold the empty state until folders resolve

Folder rows share the list with resource rows, so a workspace whose only
contents are folders has an empty `rows` until the folder tree lands — and got
the "create your first item" graphic in the gap. The resource list's own
loading gates never covered it because the folder tree is a separate query.

`useFolderNavigation` already exposes `foldersResolved` (`isSuccess &&
!isPlaceholderData`) for exactly this hazard — it guards the ancestry index
against evicting a folder id it has not loaded yet. Knowledge and tables pass
it straight through; files reads the same two flags off
`useWorkspaceFileFolders`, which it calls directly. Logs omits it, as it has
no folders.

---------

Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Waleed Latif <walif6@gmail.com>
)

The grid is authored larger than the box it fades inside — 358x160 drawn into
320x148, deliberately, so it runs off two edges. But a mask tile is sized to
the element box and `mask-repeat` starts at `repeat`, so the overflow landed in
the *next* tile at the opaque head of the gradient: a solid strip of cells
reappeared just past where the fade had finished dissolving. The frame now
clips, and every fade in the set pins `no-repeat` rather than relying on its
subject happening to fit.

Also:

- `--border-1` is a legacy alias; the new files use the canonical `--border`,
  which `resource.tsx` was already using two lines above them.
- `createIsoLineProps` returns `SVGAttributes<SVGElement>`. It never returns a
  `ref`, and `ref` was the only member forcing an element type — which had made
  the knowledge mark reach for an `SVGPathElement & SVGCircleElement`
  intersection to spread onto both. `className` moves last so no caller passes
  `undefined` positionally to skip it.
- `isResourceListEmpty` is exported from the components barrel its four callers
  already import `Resource` from, instead of being reached past it.
- `emptyState` sits after `rows` on all four tables; three had it leading.
- Two TSDoc blocks claimed things the code stopped doing: the folder graphic
  does not have three fill tiers, and the empty-state wrapper grows the slot
  but does not centre it.

Adds the predicate's unit test — it is a pure eight-clause function that
decides whether a page tells someone they have nothing, and it had none.
Verified it fails when the placeholder and folder guards are removed.
…fan-out (#6852)

* fix(knowledge): stop one env knob from setting the embedding request fan-out

KB_CONFIG_CONCURRENCY_LIMIT was read in three places with three meanings: the
document-processing queue depth, the number of embedding requests issued
concurrently inside a single embed call, and (divided by five) the in-process
document concurrency. The first two multiply — every admitted task run reaches
the embed path and opens its own fan-out — so the default put roughly a thousand
requests in flight against one provider key. A rate limit is per key, so the
pipeline held itself at the limit, and no retry policy can absorb a load its own
concurrency is generating.

Each variable is now read by exactly one consumer, which also removes the drift
that hid this: the same variable was read with a different inline fallback in
each place, and since createEnv runs with skipValidation the declared defaults
never execute, so the fallbacks were the real ones and disagreed. The divisors
are gone and the previous effective values are the declared defaults, so only
the embedding fan-out changes: 50 to 8.

KB_CONFIG_BATCH_SIZE had the same conflation between chunks-per-embedding-request
and documents-per-batch, and is split the same way.

Rate-limit rejections also discarded what the provider said about when to come
back. The response headers were dropped when building EmbeddingAPIError, so the
retry loop's support for a server-stated wait was dead code on this path and
every attempt fired blind, exhausting the budget inside a window that had not
reopened. The headers now travel with the error the way fetchWithRetry already
does for connectors, and the wait is read from Retry-After or, failing that, the
reset header for whichever limit dimension is actually exhausted. Those carry a
Go duration rather than the epoch seconds the shared connector helper expects,
so the reading lives with the provider instead of changing retry behaviour for
every connector. The retry budget is sized against a rate-limit window rather
than a blip, since a 10s ceiling clamped every stated wait below the reopen time.

* fix(knowledge): stop retrying an embedding wait we will not honor

Honoring the provider's stated wait introduced a case the retry budget could
not serve. When a provider states a reset longer than the ceiling, the loop
clamps every attempt to that ceiling, so the whole budget is spent inside a
window that has not reopened — and with five attempts at thirty seconds that
delayed the fallback provider by around two and a half minutes, where the
previous blind backoff reached it in about seven seconds.

A stated wait past the ceiling now refuses the retry outright. The error still
classifies as transient, and the fallback chain classifies separately through
shouldFallback, so the next provider is reached immediately instead of after the
budget burns down. Retrying was never going to succeed in that window, so
nothing is given up.

* fix(knowledge): measure a stated wait against the whole retry budget

Refusing to retry once the stated wait passed the per-attempt ceiling was too
blunt. Each wait is clamped individually but the attempts accumulate, so a
window a little longer than one clamped delay still reopens partway through the
budget: a 35s wait is reachable on the second attempt. Rejecting those stranded
a caller with no fallback provider, which would have recovered by waiting.

The comparison is now against the budget the attempts span in total. A window
inside it is retried and can recover; only one that outlasts every attempt is
unreachable, and that still fails fast so the fallback chain is reached at once
rather than after the budget burns down.
#6828 gave the four workspace resource lists a zero-data graphic and left the
one list a level below them still painting column headers over a blank body —
the list every user meets immediately, because creating a base does not require
a file and does not navigate anywhere on success.

The mark is a stack of sheets with the front one dog-eared and ruled. The
dog-ear is the one signifier the set does not already use: the folder is a
container and the knowledge mark is a shelf of volumes, and this has to read as
the pages inside one of them rather than as either.

It reuses the rest of the recipe — hairline contours, the surface ramp for
depth, ink mixed off `--text-secondary` because the ramp is near-white in light
mode, and a fade running the direction the stack recedes. `HAIRLINE` was
byte-identical between the folder and this mark, so it moves to a shared module
beside `mask.ts`.

Visibility goes through the same `isResourceListEmpty` the four pages use, with
one difference the call site documents: it counts the server's `total` rather
than the visible rows, because this list is paginated and an empty page 2 is a
paging position, not an empty base. The folder arguments are omitted — a base's
documents are flat.

The frame is derived from the artwork's bounds rather than a round-numbered
viewBox. The sheets step up and to the right, which left the drawn mass far
enough off-centre that the mark sat visibly right of the copy beneath it.
…all, and stop the transport from undercutting a route's own deadline (#6854)

* fix(tools): give internal routes transport headroom past their execution budget

A `timeout` param bounds the work an internal route was asked to do — the code
a sandbox runs, the upstream call a proxy route makes. The fetch around it also
pays authentication, body parsing, workspace authorization, worker acquisition,
and response serialization, none of which that budget was sized for. Arming the
client with the bare number made the caller give up at the same instant the
route's own deadline fired, so the route could never win the race and report
which part actually ran long — the caller saw an unattributable
`Request timed out` instead of `Function execution timed out after 5000ms`.

Add 30s of headroom, sized above the isolated-vm worker's own 10s startup
budget so a cold worker spawn stays inside the transport deadline rather than
aborting it. An execution abort signal, when present, still bounds the call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* improvement(executor): evaluate a condition list in one sandbox call

A condition block spent one `function_execute` round trip per branch, so a
four-branch block that fell through to `else` paid four sandbox executions
before routing. Build one script that tests each expression in order and
returns the index of the first truthy one.

Ordering and short-circuiting are unchanged: an expression is only reached once
every earlier one returned falsy, so a later expression that throws is still
never reached and the run takes the same branch it took before. The script's
`catch` reports the index it was on as data rather than rethrowing, which is
what lets the handler still name the failing branch in its error.

A batch that produces no verdict falls back to one call per branch — the path
this handler used before. That is load-bearing rather than redundant: a syntax
error anywhere in the list fails the whole script at parse time, while
evaluating one at a time only reaches, and so only fails on, the branches the
run actually takes. A timed-out or cancelled batch skips the fallback, which
would otherwise re-run every branch against the same stall.

An unrecognized reply is treated as no verdict rather than as "nothing
matched", so a garbled response cannot silently route the run down the else
path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(executor): wrap condition expressions the same way in both paths

The batched script put each expression on its own line inside `Boolean(...)`
so a trailing line comment ended before the closing parenthesis; the per-branch
fallback still inlined `Boolean(${expression})` on one line. That made the
recovery path stricter than the path it recovers — a batch that failed to parse
because of a later branch would fall back and then reject an earlier
comment-bearing branch it should have matched.

Both paths now wrap through `buildBooleanTest`, so they cannot drift again.
Also narrows the evaluation-context boundary from `Record<string, any>` to
`Record<string, unknown>`; the context is only ever serialized, never indexed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview Aug 19, 2026 9:04pm

Request Review

@cursor

cursor Bot commented Aug 19, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Medium risk from changes to condition evaluation, embedding retry/concurrency, and PDF/OCR routing that affect workflow execution and knowledge indexing at scale; empty states are mostly UI with careful gating.

Overview
Resource empty statesResource.Table gains an emptyState slot below headers when there are no rows, and isResourceListEmpty gates it so the “create your first item” graphic only appears when the list is truly empty (not loading, filtered, in a subfolder, or errored). Files, tables, knowledge bases, logs, and per-base documents each get a matching graphic and primary action via a shared EmptyState frame.

Knowledge & connectors — PDFs try the embedded text layer before OCR; long PDFs OCR in page chunks for both Mistral providers, with partial chunk failure failing the whole document. Embedding concurrency is split from document-processing queue depth, with provider Retry-After / rate-limit headers driving retries. Zero-byte connector source files are rejected via hasIndexablePayload.

Condition blocks — Branch expressions are evaluated in one sandbox call with per-branch fallback on parse failures; timeouts and aborts do not fan out into per-branch retries.

Other — ISO illustration styles move to @/components/iso; settings return URL is discarded when the workspace changed; delete-account description removed; new library article on agent orchestration frameworks.

Reviewed by Cursor Bugbot for commit 9864f5c. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This release combines connector ingestion fixes, local PDF text-layer parsing, condition-evaluation batching, embedding concurrency and retry improvements, settings navigation fixes, and new resource empty states.

  • Rejects zero-byte connector source files before indexing.
  • Tries embedded PDF text before OCR and adds chunked Azure OCR for large PDFs.
  • Evaluates condition branches in one sandbox request with sequential fallback.
  • Separates document, queue, and embedding concurrency configuration and honors provider retry windows.
  • Adds empty-state experiences for knowledge, documents, files, logs, and tables.
  • Adds an AI-agent orchestration library article and shared isometric illustration styles.

Confidence Score: 4/5

The PR appears safe to merge after addressing the non-blocking inline-style convention violation in the new resource empty-state illustrations.

The changed runtime paths preserve fallback and cancellation behavior, while the only accepted issue is the use of JSX inline styles instead of the repository’s required Tailwind styling approach.

Files Needing Attention: apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/logs-empty-state.tsx and tables-empty-state.tsx

Important Files Changed

Filename Overview
apps/sim/executor/handlers/condition/condition-handler.ts Batches ordered condition evaluation into one function execution while retaining per-condition fallback for malformed batch verdicts.
apps/sim/lib/knowledge/documents/document-processor.ts Adds local PDF text-layer triage and shared chunked OCR behavior for Mistral and Azure providers.
apps/sim/lib/embeddings/client.ts Separates embedding request concurrency and expands retry handling to respect provider rate-limit reset signals.
apps/sim/connectors/utils.ts Adds a shared payload check that rejects zero-byte source files while preserving nonempty extracted text.
apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/logs-empty-state.tsx Adds the logs empty-state illustration but introduces inline JSX styles contrary to the repository styling rule.
apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tables-empty-state.tsx Adds the tables empty-state illustration and repeats the inline-style pattern covered by the consolidated finding.
apps/sim/tools/index.ts Adds transport headroom beyond internal routes’ requested execution budgets while preserving caller-driven cancellation.

Reviews (1): Last reviewed commit: "improvement(condition): batch condition ..." | Re-trigger Greptile

…load (#6858)

* fix(knowledge): say what went wrong when a document's chunks fail to load

`combinedError = documentError || searchError || initialError` collapsed three
different failures into one, blanked the rows and stripped search, sort and
filter — and said nothing. A failed read rendered as an empty table, which is
the same thing the page shows when a document genuinely has no chunks.

Stripping the search box was the worse half: when it was the *search* that
failed, the control the user needed to clear it was the one that disappeared.

The three are now told apart:

- The document itself failing has no page left to draw, so it gets a full
  screen, matching the base page's 'Knowledge base not found' one level up.
  It has to run before the editor branches — `selectedChunkId` renders the
  chunk editor without checking for a document, so a deep link to a chunk of a
  deleted document sat on 'Loading chunk…' forever.
- A failed chunk read keeps the document, so it keeps the chrome and the
  controls, and the message goes in the table body through the `emptyState`
  slot. Tinted with the error token, because at the weight the empty states use
  a failure is indistinguishable from 'nothing here yet'.
- A failed search leaves the loaded chunks intact, so it says the search
  failed rather than claiming the chunks could not be loaded.

`searchError` went through `instanceof Error ? .message : null`, so a rejection
that was not an `Error` produced no message and fell back to the silent blank
this commit exists to remove. It uses `getErrorMessage` now, like the chunk
read beside it always did.

Pagination is dropped on a failed read — it was counting pages nothing fetched
— and the action bar reads the same value, so it no longer lifts itself clear
of a bar that is not there.

The not-found screen was about to be copied a second time, so it moves to
`ResourceNotFound` and the base page adopts it.

* fix(knowledge): let a processing document say so, not that it failed

A document that is not `completed` rejects the chunk read by design —
`requireChunkReadable` throws `KnowledgeDocumentNotReadyError` before it queries
anything — so `initialError` is set for every pending, processing or failed
document. Treating that as a load failure put "Couldn't load chunks" over a
document that is simply still working.

`chunkRows` already builds the right row for those states, and it turns out
nothing could ever see it: the old `combinedError` blanked the rows on exactly
the same condition, so "Document processing pending..." has been unreachable
for as long as it has existed. Excluding not-ready documents from `chunkError`
brings the row back and leaves the error state for reads that genuinely failed.
waleedlatif1 and others added 3 commits August 19, 2026 13:55
… bodies (#6861)

* improvement(search): search every folder, and document real API error bodies

Search on Files, Tables, and Knowledge was ANDed with the open folder, so a
query only ever matched that folder's direct children — and the query was not
cleared when you entered a folder, filtering the folder you just opened down to
the same matches. A non-empty query now searches the whole workspace, a
Location column names each result's folder, and opening a folder ends the
search.

Also gives GET /api/v2/files a `recursive` flag, and replaces the single shared
OpenAPI error example — which showed `BAD_REQUEST` under every status tab — with
one real body per status.

* fix(search): discard the search term on clear instead of masking it

`useSearchFilterValue` returned the debounced term whenever the input was
non-empty, so clearing only hid the settled needle. The mask lifted on the next
keystroke while the debounce still held the pre-clear term — opening a folder
and typing within the window searched the whole workspace for the query the
user had just abandoned.

A clear now resets the settled term rather than hiding it, adjusted during
render so the reset is visible to the render that follows the clear. The
initial state is seeded from the first value so a deep-linked `?search=` still
filters on the first render.
* fix(branding): refresh Google logo

* refactor(branding): trim Google icon tests and correct the SVG wrapper

Drop the GoogleIcon and SocialLoginButtons snapshot tests: they pinned exact
attribute strings, the asset byte length, and the absence of markup the
component never contained, so they broke on any legitimate tweak without
catching real regressions.

Correct the wrapper's viewBox to 0 0 200 204 so it matches the artwork, which
bleeds to all four edges. The previous 204-wide box pinned four units of dead
space to the right via xMinYMin, offsetting the mark within its box.

Rewrite the TSDoc: it described avoiding a WebKit foreignObject gradient bug,
but this file never used foreignObject and already ships 106 linearGradient
definitions. Document the real reason instead - Google publishes the current G
only as a raster.

Align the auth button icon on shrink-0 with its sibling callsite.

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Waleed Latif <walif6@gmail.com>
… button (#6862)

The login and signup social buttons sit under an "Or continue with" divider,
so "Continue with Google" read as "Or continue with Continue with Google" —
and its siblings on the same stack are bare "GitHub" and "Microsoft". Restore
"Google" and hand the icon back to the chip's own leftIcon slot so it picks up
the canonical 16px chipContentIconClass instead of a className size override.

The landing auth modal is a different surface: every button there reads
"Continue with X" (Microsoft, GitHub, email), so its Google label is correct
and stays. Only its icon changes, 20px back to the 18px both siblings use.
@waleedlatif1
waleedlatif1 merged commit 8273320 into main Aug 19, 2026
54 of 55 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.

4 participants