Skip to content

chore: sync dev with staging (2026-11-03) - #9604

Merged
waleedlatif1 merged 153 commits into
devfrom
chore/sync-dev-with-staging-2026-11-03
Nov 3, 2026
Merged

waleedlatif1 merged 153 commits into
devfrom
chore/sync-dev-with-staging-2026-11-03

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Brings dev up to current staging so the dev environment matches what ships next (also needed to run the worker benchmark against current code on dev). The merge result is identical to staging (no dev-only content); migrations included are ones staging and prod already ran.

waleedlatif1 and others added 30 commits October 28, 2026 14:41
… @opentelemetry/sdk-trace (#9421)

@opentelemetry/sdk-trace-base and @opentelemetry/sdk-trace-node are deprecated
in favor of @opentelemetry/sdk-trace. Import the trace SDK from
@opentelemetry/sdk-trace 2.10.0, matching the installed core and resources
2.10.0 and staying on @opentelemetry/api 1.9.x so the global API stays shared
with the Trigger.dev runtime.

BatchSpanProcessor and SimpleSpanProcessor take an options object with the
exporter, and BasicTracerProvider is TracerProvider. Every batch option is set
explicitly, so the dropped OTEL_BSP_* environment fallbacks change nothing.
…keys (#9423)

Map TRIGGER_ACCESS_TOKEN from TRIGGER_ACCESS_TOKEN_PROD/_STAGING/_PREVIEW,
falling back to the shared TRIGGER_ACCESS_TOKEN until those secrets exist.
Bump trigger.dev CLI and @trigger.dev/* to 4.7.6, the first line where
promote accepts environment API keys (4.7.0) and a mismatched --env errors
instead of deploying to the key's environment (4.7.5).
…9419)

* fix(billing): revoke a removed member's sessions after the workspace handoff locks

Member removal bumped the organization's security policy version, which
row-locks the organization, and only then locked every organization
workspace row for the shared-resource handoff. Storage writers lock a
workspace row first and then its organization payer row, so a removal
racing an upload or delete in one of the organization's workspaces
deadlocked. Session revocation now runs after the handoff holds the
workspace rows, keeping workspace rows before the organization row.

* perf(billing): admit wand and chat-title model calls through the usage gate cache

Both are single model calls gated on the payer's period usage, and both
summed the ledger on every request. They now use the execution gate
policy: a refusal is always re-read, and an admission is reused for at
most the near-limit TTL when headroom is thin. A new chat turn's dispatch
and the copilot validate callback keep reading the gate fresh.

* perf(workflows): withhold the deployed state blob when this process already caches it

loadDeployedWorkflowState selected the full state json on every call and only
then consulted the in-process cache keyed by the immutable deployment version
id, so a cache hit still shipped the whole snapshot from Postgres. The read now
remembers which version each workflow last cached and asks Postgres to return
NULL in place of state for that id, so a hit and a miss both stay one round
trip and only a miss carries the blob. A hint left behind by an evicted entry
falls through to the existing by-id read. The unused created_at projection is
dropped too.

An integration test pins the cold, warm, cutover, evicted-hint and undeployed
paths against real Postgres.

* perf(audit-logs): read each scope branch through its keyset index

The organization audit listing filtered on one predicate, workspace_id IN
(org workspaces) OR (workspace_id IS NULL AND names the org), ordered by the
millisecond-truncated timestamp. No index can return that OR in order, so
Postgres collected and sorted every row in scope to return one page; for a busy
organization the plan is a parallel sequential scan of audit_log, which on the
replica runs long enough to be cancelled by recovery conflicts.

The scope is a disjoint union, so the page's ids now come from one backward
scan of audit_log_workspace_created_at_id_idx per workspace (a lateral join
over the workspace ids) plus the organization-level rows, each stopping after
limit + 1 matches; the first limit + 1 of their union are exactly the first
limit + 1 of the whole scope, and the rows are then read by primary key in the
same order. The keyset cursor becomes the equivalent row comparison, which the
index scan seeks to directly instead of walking every newer row. queryAuditLogs
takes the scope structurally; buildOrgScopeCondition is unchanged for the
single-record read and the data drain.

An integration test pages through mixed workspace, organization-level, foreign
and departed-actor rows with sub-millisecond ties at several page sizes, with
and without the byte budget, and asserts the exact sequence the single-predicate
query returns.

* perf(tables): fetch only the mutated workspace's table triggers

fireTableTrigger runs after every committed table row mutation and loaded every
active table-trigger webhook in the deployment, joined with its full workflow
row, only to discard all but the mutated workspace's in JavaScript. It is the
most frequent webhook query in production and its largest egress source.

fetchActiveWebhooks takes an optional workspace scope that becomes a
workflow.workspace_id predicate, and the table trigger passes its workspace, so
a mutation in a workspace without table triggers reads nothing. The in-memory
workspace check stays as a second guard. An integration test pins the scoped
result to the unscoped result restricted to the workspace.

* perf(audit): hold a run's rollups for a minute instead of two seconds

A workflow run's repeated table row changes and credential accesses merge into
one audit row only while they fall inside the rollup window, two seconds after
the first. Repeats spaced further apart, which is most scheduled runs that loop
over slow tool calls, each opened a new row, so a run wrote several rows per
table and credential where the audit log docs describe one.

Inside a run's rollup scope, whose end writes every pending row anyway, the
window is now a minute: a run's repeats share a row however far apart they
fall, and a run whose process dies loses at most a minute of merged entries,
within the best-effort durability the docs state. Rollups recorded outside a
run keep the two-second window, and the per-row entry cap is unchanged, so no
identifier or count is dropped.

* perf(file-search): lock only the files a backfill page actually seeds

The backfill walk locked every file of its page FOR SHARE, and its idempotent insert
fired the revision owner trigger, which locks the file again, for every file that
already had a revision. Each pass wrote a row lock into every live file's heap tuple.

Walk without locks, check for an existing revision per row, and lock and reread only
the files still missing one before inserting. The lock still keeps a content change
from committing between the read and the insert, and a file deleted since the walk is
now skipped instead of failing the page. Every owner the page passes is still woken.

* perf(knowledge): count a completed listing window by window

A completed listing counted the connector's owned, listed, absent and tombstoned
documents in one statement over every owned document. On the largest connectors that
single read outlasts the statement timeout, so the reconciliation pass failed before
it could walk anything.

Sum the counts over the same bounded id windows the reconciliation walks scan, one
statement per window, with the lease heartbeat between windows and the run deadline
ending an unfinished count the way it ends an unfinished walk.

* perf(files): coalesce workspace list invalidation broadcasts per tab

Every file mutation broadcasts a list-changed event to each open browser in the
workspace, and each one invalidated the whole workspace file list at once. An API
client uploading files one by one made every viewer refetch the full list once per
upload, and invalidating a query mid-fetch cancels and restarts it, so a large
workspace's list could be read many times over without one response landing.

Deliver a room's changes one at a time: the first change is delivered at once, and
changes that arrive while a delivery's refetch is in flight, or within a second after
it settles, fold into one delivery after that.

* fix(copilot): purge the copilot backend and stored files when a chat is hard-deleted

DELETE /api/copilot/chat/delete removed the copilot_chats row but never told the
copilot backend, so the backend's chat, events and runs for that chat were orphaned
for good, and the chat's copilot/inline-image storage objects were never deleted.
deleteCopilotChat now collects the chat's file refs with prepareChatCleanup before
the delete transaction (the message rows that name them cascade away with it) and
runs execute() after commit, which re-checks the row is gone, calls the backend's
/api/tasks/cleanup and deletes the files. A failed purge is logged and does not turn
the committed delete into an error response.

Autopsy: the only callers of the backend cleanup were the retention jobs and the
organization cleanup, and no test covered deleteCopilotChat's side effects outside
Sim's rows: the integration suite only asserted that it stopped live runs. The new
deletion.test.ts asserts the purge is prepared for the chat and executed after the
delete, and fails on the previous code.

Adds a central @sim/testing mock for @/lib/cleanup/chat-cleanup (its fourth test
user) and moves the three existing local factories onto it.

* fix(copilot): queue copilot backend cleanup for chats an account removal cascades away

copilot_chats cascades from user, workspace and workflow (and workflow from user and
workspace), so deleting an account through POST /api/users/me/deletion, or a user row
through /admin/remove-user, erased chat rows without ever telling the copilot backend,
which kept those chats' data forever.

A new outbox event, copilot.chat.backend-cleanup, carries up to 100 chat ids. Its
handler purges only chats whose rows are gone and throws on a failed backend call, so
the outbox retries it. Account deletion collects the chats its workspace and user
deletes cascade (the account's own chats, chats in the doomed workspaces, and chats on
workflows the account created or those workspaces hold) and queues the events in its
transaction, so a rollback queues nothing. /admin/remove-user deletes outside any
transaction, so its Better Auth delete hooks read the chat ids before the delete and
queue the events after it commits, as they already do for run stops. Nothing is queued
without COPILOT_API_KEY, matching memory cleanup.

Workspace and workflow rows have no other hard-delete path: the UI archives them, and
the soft-delete retention job already runs prepareChatCleanup for the workflows it
purges.

Autopsy: the account-removal integration suites asserted run stops, storage and memory
cleanup, but no test looked at what the copilot backend kept for the chats the
cascade erased. chat-backend-cleanup.integration.ts drives both removal paths against
real PostgreSQL and a local backend fixture and asserts exactly the erased chats are
purged; its account-deletion case fails on the previous code.

* improvement(outbox): prune completed checkpoint expiry, file, memory and chat cleanup events

Retention pruned completed rows of only two event types, so outbox_event kept every
other completed row forever; the embedding checkpoint expiry events alone are most of
the table. Completed rows older than the 7-day window are now also pruned for:

- the three checkpoint expiry events. They are deterministic-id gates: a writer inserts
  the row before uploading and skips the upload once the row is no longer pending. The
  row completes at the checkpoint's 48-hour expiry, and after it is pruned the next
  writer of that key inserts a fresh pending cleanup before uploading, so no object is
  left without a cleanup. The new integration case drives writeFramedCheckpoint through
  completion, pruning and a rewrite to prove it.
- workspace-file.live-doc.reconcile, workspace-file.storage.cleanup,
  mothership.memory.cleanup and copilot.chat.backend-cleanup: random ids, looked up only
  by the immediate post-commit process call while pending.

Still kept: workflow deployment and workspace operation events (workspace operation
receipts read their effect rows back by id and treat a missing row as a failed
effect), enterprise/admin operation records, idempotency-keyed events, and every
pending, processing and dead-letter row. The per-type batch and cadence are unchanged,
so an existing backlog drains over successive maintenance runs.

* perf(idempotency): drop the full-table stat scans and resume each cleanup batch where the last ended

The daily /api/webhooks/cleanup/idempotency run read the whole idempotency_key table
four times for its before/after stats (count/min/max and a GROUP BY namespace, twice),
which only fed two log lines and the cron response. It now reports what it actually
deleted per namespace, counted from the delete's returned keys, and an estimated row
count from pg_class.reltuples (one catalog read) after the sweep.

The sweep itself started every batch from the oldest expired row. The global sweep
keeps the permanent admin-credit-grant and workflow-execution-id claims, whose expired
rows sit at the front of the created_at order forever, so each 1,000-row batch walked
all of them again before finding anything to delete. Batches are now ordered by
created_at and resume at the timestamp the previous successful batch ended on (>=, so
keys sharing that timestamp are not skipped), and the retained rows are read once per
run instead of once per batch.

The new integration test sweeps expired keys across several small batches, including
a tie on the boundary timestamp, and asserts every expired key goes while the
permanent and fresh ones stay; it fails if the resume skips the tie.

* perf(db): drop prefix-covered indexes and vacuum churn-heavy tables sooner

Drop 32 b-tree indexes whose columns are a leading prefix of another
non-partial b-tree on the same table, so the remaining index serves every
lookup, range scan and foreign-key check they did. Each coverer is named
beside its drop in the migration. Indexes whose only longer sibling is
partial (user_table_def_workspace_id_idx, workflow_mcp_tool_server_id_idx)
stay, since a partial index cannot serve FK checks.

Lower the vacuum and insert-vacuum scale factors to 2% on embedding,
workspace_files and copilot_async_tool_calls, which accumulate dead and
unvacuumed rows well past the 20% defaults between runs. embedding_search
is left alone: each vacuum there also scans its HNSW index.

* perf(db): cover usage_log period sums with one index led by both period stamps

The period-sum read (entity plus both period stamps) bounded its scan on
only three columns of the covering index, because user_id and created_at
sat between the two stamps. With the planner also underestimating that
match (the stamps are functionally dependent on the entity), it chose the
slimmer index without cost and fetched every matched row from the heap.

One index keyed (entity type, entity id, period start, period end,
user_id) with created_at, source and cost as trailing payload now serves
the period sums, the per-user sums, the weekly-refresh buckets and the
cycle-close range index-only, and replaces both previous indexes.

A replay after a failed build moves only an invalid copy of the new index
aside to be dropped, so a valid index an earlier attempt built is kept
rather than rebuilt.

* chore(outbox): read the retention suite's database through a typed handle

The checkpoint-gate case added three more non-null assertions on the hoisted database holder. The suite now keeps the drizzle handle it builds and reads through it, so the file carries none and its explicit-any baseline entry goes.

* improvement(files): drop the unobserved folder-tree room callback memo and a pre-existing-behavior test

useWorkspaceInvalidationRoom reads its callback through a ref and no effect lists it, so the folder tree's useCallback bought nothing. The cross-workspace broadcast test guarded a filter the coalescing change left untouched.

* revert(billing): read the usage gate fresh for wand and chat-title model calls

Reverts c921209. The cached admission it routed both calls through also
caches the payer's billing-block result, so a payer blocked for payment after
an admission was cached still passed for up to the cache TTL. The saved ledger
sum per call was small next to that. The requestChatTitle test's cache reset
existed only for that cached path and goes with it.

* fix(copilot): record a deleted chat's backend and file purge durably with its delete

A permanent chat delete purged the copilot backend and the chat's stored files
after commit and only logged a failure, so a failed purge was lost: the chat row
was gone and nothing could find it again. /admin/remove-user read the cascaded
chats in its before hook and queued their backend cleanup in its after hook,
logging either failure and letting the removal proceed with nothing recorded,
and neither removal path purged the stored files of the chats it erased.

The copilot.chat.backend-cleanup event now also carries the chats' stored-file
keys, collected while the rows naming them exist (100 chat ids or 100 files per
event). Its handler purges the backend data and deletes the objects of chats
whose rows are gone, and throws on any failure, an already-missing object
excepted, so the outbox retries; chat ids reaching a processor without
COPILOT_API_KEY throw as the organization cleanup does. File events are queued
even without the key.

- deleteCopilotChat reads the keys under the chat row's lock and queues the
  event in its transaction, so the purge is recorded exactly when the delete
  commits; the post-commit best-effort purge is gone.
- Account deletion reads the chats and keys before its transaction, as its
  storage sweep does, re-reads the chat ids under the account lock, reads the
  keys of chats created since, and queues it all in the transaction.
- The admin remove-user before hook queues the event before the delete, due an
  hour later so the delete has committed; a failed read or enqueue throws,
  which makes Better Auth abort the delete. A delete that never happens leaves
  the chats alive, and the handler purges nothing for them.

collectChatFiles moves to chat-files.ts so the auth graph does not load the
backend and storage clients; it still adds five modules to the routes that
import auth, and their module baselines are re-recorded.

Autopsy: deletion.test.ts only asserted calls on a mocked prepareChatCleanup,
so it could not see that the purge was not durable, and the integration
stand-in accepted any request. The integration suite now drives the real chat
delete, account deletion and admin hook against PostgreSQL, local storage and
a stand-in that refuses anything but an authenticated POST /api/tasks/cleanup:
a rolled-back delete records nothing, a failed backend call or object delete
is retried until it succeeds, a missing key holds the backend purge, and a
failed enqueue refuses the admin removal. Seven of its eight cases fail on the
previous code.

* docs(knowledge): say what the windowed reconciliation counts do and do not guarantee

The per-window sums are not one snapshot. They differ from one only by the
rows writers outside the sync lease change during the count, and they only
gate whether a walk runs: every soft and hard delete re-checks absence in its
own statement, as it already had to after the single count statement.

* chore(copilot): prove the cleanup rollback tests fail on the fixture trigger, not any error

* docs(auth): state what the admin removal cleanup hook cannot cover

* revert(audit): keep a run's rollup window at two seconds

Reverts 03ca9c5. A rollup holds entries in process memory until its window closes, so a minute-long window widens what a crashed process loses from the audit trail thirty-fold. Platforms that aggregate audit events summarize records already written durably; keep the short window.

* perf(billing): bound cycle close's previous-period lookup below the elapsed period

The covering usage_log period index added in 0441 has unique keys, so B-tree deduplication can't compress an entity's current period the way the old four-column index did, and max(billing_period_start) for the previous period walked every current-period entry first. The elapsed period starts before it ends, so bounding the start below it changes no answer and lets the scan skip the current period.
…rate (#9425)

* feat(db): seal account tokens by default and backfill them during migrate

* fix(db): resolve sealing env on the migrator like the app and defer only on batch timeouts

* fix(helm): require the app's ENCRYPTION_KEY on the migrations init container
Each room delivery waits at most DELIVERY_SETTLE_CAP_MS (one minute, above the slowest list read) for its refetches via withinDeadline, so a refetch that never settles no longer stops every later delivery; past the cap, the next delivery's invalidation cancels and restarts a hung refetch of a query with data. Settling refetches are still waited for and never overlapped.
The first hunk of patches/zod@4.3.6.patch created an empty .bun-tag-* file
left over from regenerating the patch, with mismatched a/ and b/ paths.
Bun writes its own tag on apply, so the hunk only added a second marker file.
The size-check hunks are unchanged.
…as signups can't both keep it (#9431)

* fix(billing): serialize Gmail inbox allowance decisions so racing alias signups can't both keep it

* fix(billing): restore lock_timeout after taking the Gmail inbox lock
* fix(scim): harden directory lifecycle and clarify group mappings

* fix(scim): close provisioning races and refresh policy state
Adds workspace_files_workspace_active_uploaded_idx (workspace_id, uploaded_at, id) on the shared live workspace-file predicate, built concurrently with replay-safe recovery, so the file list and its bounded SSR seed read a workspace's rows in upload order instead of matching every workspace's files and sorting. listWorkspaceFiles orders by (uploaded_at, id) for a stable order. Adds @sim/db/testing/explain for plan assertions on the SQL a call actually sends.
…and make room refetches wait (#9433)

Release-review fixes for #9419/#9427: room callbacks must return their invalidations (tables and workflows rooms now wait for their refetches); the deployed-state hint is set only by active reads; chat cleanup keeps any chat-scoped key a surviving chat in its scope still attaches (one query per scope) and collects a forked workspace upload once no file row names it; inline images are swept by their chat prefix through their own outbox event; malformed attachment keys are skipped. Legacy queued payloads are still accepted.
…he session API (#9435)

Session endpoints for bounded workspace-file pages (folder, sort, cursor), name search, batched lookup and folder rollups, behind one shared page implementation also used by v2 and the mothership VFS; the unpaged list response is unchanged. Folder pages seek the folder-name index via coalesce(folder_id,''); timestamp keysets order on the plain column with microsecond cursors (legacy millisecond cursors still resume). References that aren't valid percent-encoding are read as bare names. Migration 0443 adds a folder upload-order index and a trigram name index (fastupdate off), built concurrently.
… the workspace (#9436)

Moves every workspace-file consumer off the full workspace list onto the paged, search and lookup reads: the Files browser is a virtualized infinite table with server-side sort (incl. Type and Owner), kind/size/uploader filters, ranked search and folder sizes; single-file views read the record by id; chips, names, image dimensions and stale-key recovery use batched lookups; pickers, mentions and Cmd+K use one recent-or-search hook; the sidebar flyout loads folders lazily. Type and kind rules are shared with the project file browser in JS and SQL. Writes patch only reads they can't reorder; the table separates its load-more spinner from the sentinel gate and waits for re-entry after a failed page. The unpaged list response and the public v1 API are unchanged.
…keyset parts (#9437)

Fixes a regression from #9419: the organization-level branch of the audit-log listing had no index serving it in order, so a listing walked every workspace-less audit row. Migration 0444 adds a partial keyset index on workspace-less rows that name an organization, and the branch is split into two disjoint keyset parts (metadata through the new index, organization-resource rows through the resource index). A plan test pins the index use.
…scope predicate again (#9438)

#9419 read a listing's page ids as one keyset scan per organization workspace plus the
organization-level rows. That bounds an unfiltered listing, but a filter the workspace
keyset index cannot apply in order (an action, a resource, an actor) turns each
workspace's scan into a bitmap AND of that workspace's whole history with the filter's
index, then a sort, once per workspace. For a busy organization a filtered listing went
from one scan of the filter's index to many times that, and slow replica reads are what
recovery conflicts cancel.

Restore the listing to the single organization-scope predicate it used before, which lets
the planner pick the filter's index, keep the org-level index from 0444 (the predicate's
organization-level rows can use it), and keep the scope integration test against the
restored signature. The per-workspace plan test described the reverted shape and goes.
#9428)

* feat(search): move assistant into organization search

* feat(search): add personal assistant actions and per-tool permissions

* chore(search): finalize migration and validation artifacts

* chore(search): align regression fixtures with account permissions

* chore(tests): make asynchronous validation deterministic

* fix(db): rebase assistant migration and preserve atomic expansion

* docs(search): show dedicated organization search navigation

* fix(search): preserve drafts and harden reviewed action delivery

* fix(search): finalize refusal stream contract and validation

* test(search): assert credential rejection independently of wording

* fix(search): align review limits and isolate OAuth token refresh

* fix(docs): preserve spacing above nested sidebar sections

* docs(search): remove stale mode instructions and clarify actions

* fix(search): distinguish unavailable account connections

* chore(search): validate rebased migration and module graph

* fix(tests): wait for durable text before recovery faults

* fix(tests): wait for Search URL persistence before reload
* docs(library): update ai-agent-vs-chatbot

* Pi Babysit: address PR #9441 feedback

---------

Co-authored-by: Sim Pi Agent <pi@sim.ai>
…ions Compared (#9443)

Co-authored-by: Sim Pi Agent <pi@sim.ai>
#9445)

The backfill walk checked each file for a revision with a correlated EXISTS
in its select list. PostgreSQL plans that as an alternative subplan and picks
between a per-row probe and a hashed subplan from the scan's own row estimate,
before the page limit applies. Sized against every file left in the walk, the
hashed form wins, so every page sequentially scanned and hashed the whole
revision table to answer a thousand lookups.

The page now left-joins the revision on its primary key, the file id, and
reads hasRevision as the joined key being non-null. Under the limit the
planner nests that join over the ordered keyset scan, one index probe per file
returned. The key is unique, so the join cannot repeat a file, and the keyset
predicate, order and limit are unchanged.

On a production-sized replica the page went from about 500ms with a full scan
of the revision table to about 12ms warm with index probes, with the keyset
index scan on workspace_files unchanged.

The new integration case walks a second page over a settled table and asserts
the revision table is only read through its primary key, probed per file; it
fails on the previous query with the hashed sequential scan.
…tch ended (#9446)

* perf(outbox): resume each retention batch where the previous run's batch ended

The completed-event prune deletes one oldest-first batch per type per run.
Every batch started its scan of outbox_event_type_created_idx at the oldest
row of its type, and the rows earlier batches deleted stay in that index until
vacuum removes them, so while a large backlog drains each batch first walks
everything deleted since the last vacuum. That walk grows with each run: on a
read replica during a large drain it took about 0.6s before the first live
row, and a batch can reach the 10s statement timeout.

Each batch now resumes after the (created_at, id) of the last row the previous
run's batch deleted, the same shape as the idempotency sweep's resume. Batches
are ordered by (created_at, id) so ties on created_at split exactly, the cursor
keeps PostgreSQL's microsecond text form, and the row comparison becomes a
created_at >= bound on the index, with the full comparison as a filter.
Resuming from a cursor, the same batch took about 20ms.

The prune runs once per processor run, so the cursor has to outlive the run;
it is kept in Redis as a hint, one MGET before the batches and one pipeline
after. A row the pass went by before it completed, or while an overlapping run
held its lock, is covered by the next pass: the cursor is cleared when a batch
comes up short of the cutoff, and a pass older than an hour starts over from
the oldest row. Without Redis, or with an unreadable cursor, a batch starts
from the oldest row as before. Batch size, timeout, cadence and which rows are
eligible are unchanged.

Autopsy: the retention suite asserted which rows each run deleted, which a scan
from the oldest row gets right, so nothing observed where a batch started. The
new Redis cases complete a row behind the cursor between runs: resuming leaves
it for the next pass and the old scan takes it at once, so the first case fails
on the previous code. The others split one creation time across batches with
pending rows mixed in, and restart a pass that is over an hour old or whose
cursor is unreadable.

* fix(outbox): start a pass from the oldest row when its stored cursor names no real time

A cursor whose JSON parsed but whose creation time was not a calendar time
reached the batch's ::timestamp cast and failed the run, which stopped every
later type's prune until the cursor expired. The cursor's creation time is now
checked as PostgreSQL's ISO text with a calendar round trip, so such a cursor
is dropped like an unreadable one and the pass starts from the oldest row.
…rkspace keyset scans (#9450)

* perf(audit-logs): read a listing narrowed only by time through per-workspace keyset scans

An organization's audit listing is one OR predicate over the organization's workspaces and its
organization-level rows. With no filter, or only a time range (the audit page's default view),
PostgreSQL has no index that returns that OR in listing order, so it collects every row of the
scope inside the range and sorts it for each page; for a busy organization that is a scan of a
large part of the table.

Such a listing now reads its page ids as a disjoint union: one keyset scan of
audit_log_workspace_created_at_id_idx per organization workspace, the organization-level
metadata rows through audit_log_org_level_created_at_id_idx, and the organization resource rows
that do not name it in metadata. Each part stops after a page, and the first page of the union is
exactly the first page of the scope. The time range is kept verbatim and also stated as implied
bounds on the sort key, so each scan starts and stops at the range; the cursor is the same keyset
position written as a row comparison so the scan seeks to it.

A listing with any other filter (action, resource, actor, workspace, search) keeps the single
predicate unchanged, byte for byte: per-workspace scans cannot stop early under such a filter,
which is what made #9419 slow, while the single predicate lets the filter's own index drive.
queryAuditLogs now takes the scope and the filters rather than prebuilt SQL so it can make that
choice.

list-scope.integration.ts pages every filter shape, with and without departed members, across
page sizes and the byte budget, and requires exactly the single predicate's order, including
sub-millisecond ties, bounds inside a millisecond and a workspace the organization no longer
holds. list-plans.integration.ts pins the keyset plan for the unfiltered and time-bounded shapes
(failing on the previous single-predicate listing) and the single predicate for every other one.

* test(audit-logs): plan the keyset listing over a workspace written mostly by a departed member
…9447)

* perf(db): serve workspace usage drill-downs index-only on usage_log

Every Workspaces drill-down read filters on the billing entity, one
workspace and a created_at window, and reads cost with user_id, source or
workflow_id. usage_log_workspace_created_at_idx held only (workspace_id,
created_at), so the planner matched the window there or on
usage_log_billing_entity_created_at_cost_idx and fetched a heap row per
match to check the rest: the workspace's rows on one, the whole
organization's on the other. 0441 dropped the compact entity/period index
that used to narrow the second through a BitmapAnd, so a large
organization's workflow ranking went from about 4.6s to 13.7s, reading
thousands of rows per row it returned.

usage_log_workspace_created_at_cost_idx replaces it: the same leading
(workspace_id, created_at), then the entity and every column the
drill-down reads. Each drill-down read is now an index-only scan bounded by
the workspace's rows in the window, and the readers that name only the
workspace (the foreign key's ON DELETE SET NULL, the ledger list filtered to
a workspace) keep the order they had. The new index is built concurrently
before the old one is dropped, so inserts maintain the same number of
indexes.

The plan integration test explains the exact SQL of the totals, series and
rankings for a workspace holding most and a small share of its
organization's usage, and fails on the old schema.

* perf(db): scope the workspace covering index's claim to created_at windows
…9448)

* perf(admin): read dashboard spend and workflow runs index-only

The dashboard's per-entity usage was one statement OR-ing every entity's
period, which the planner can only answer with a BitmapOr and a heap fetch
per matched ledger row, and its count(distinct execution_id) needed the heap
anyway because no index carries execution_id.

Each read is now a UNION ALL with one arm per entity, so spend is an
index-only scan on the existing covering cost indexes, and workflow runs are
a separate statement on a new partial index over workflow rows that carries
execution_id. The four copies of the period predicate now come from
buildUsageAnalyticsScope. What the dashboard shows is unchanged: a run is
still a distinct execution_id among the entity's workflow rows in the period.

The reads stay on the primary: the replica serves these scans from disk and
cancels statements held past its replay delay.

* perf(admin): count dashboard workflow runs on the existing ledger indexes

Drops the workflow-runs index and its migration: an extra write on every
workflow ledger insert is not worth paying for a staff-only page. Spend stays
index-only per entity; the separate run-count statement fetches the matched
workflow rows from the heap on the existing period and created_at indexes.
…indows (#9449)

The reconciliation count read every window of a connector, then the ACL,
soft-delete and hard-delete walks each read the same windows again. The tally
now counts every decision input and keeps the rows each removal acts on in the
same window statement, so removals page through those rows instead of walking
the connector again. A condition matching more than 10,000 documents keeps only
its count and falls back to the existing walk, so memory and each statement's
result stay bounded.
…them running (#9452)

* improvement(sandbox): pause idle chat workbenches instead of keeping them running

- Session sandboxes now pause after a 5-minute idle lease (was: kept running for
  20 minutes, then killed). Resume keeps files, packages and processes and takes
  well under a second; paused sandboxes are not billed and do not count toward
  the provider's concurrency limit.
- A new cleanup-session-sandboxes cron kills workbenches paused for more than 15
  minutes, so a workbench is still discarded about 20 minutes after its last use.
  Each kill runs under the session lock and re-checks the sandbox's own info,
  so a just-resumed workbench is never killed.
- Streamed workbench writes and upload snapshots lease their full transfer
  budget so a machine cannot pause mid-transfer.
- E2B Mothership sandboxes are priced at their template's 2 vCPU / 1 GiB
  instead of the 4 GiB Function profile.

* chore(helm): bump chart version for the session sandbox cleanup cron

* fix(sandbox): bound the session sandbox sweep by a deadline, not a batch size
- Pi sandboxes are built at 4 vCPU / 8 GiB (Daytona adds a 10 GiB disk) but
  were priced on the 2 vCPU / 4 GiB Function profile, so Pi runs were billed
  at about half their provider cost.
- Pi sizing moves from the build-script package list into
  lib/execution/remote-sandbox/pi-resources.ts, which both provider builds and
  sandbox pricing now read, so the build and the price cannot drift apart.
waleedlatif1 and others added 23 commits November 2, 2026 12:29
…nt Triggers (#9572)

Co-authored-by: Sim Pi Agent <pi@sim.ai>
… Workflows (#9571)

Co-authored-by: Sim Pi Agent <pi@sim.ai>
All four workflow run tools reach the same executor, but the catalog only
gated run_workflow and run_workflow_until_block. The approval gate is a pure
catalog lookup, so an agent steered into run_from_block (which runs every
block downstream of its start) or run_block executed the workflow with no
approval card. Flag both siblings and assert every tool in
WORKFLOW_TOOL_NAMES carries requiresApproval.
… command line (#9575)

* fix(copilot): require approval for terminal input, which can submit a command line

* fix(copilot): claim an ungated terminal operation in the desktop Stop fixture
…ms (#9580)

* fix(realtime): enforce the Files capability on workspace document rooms

* fix(realtime): pass the route context in the document access integration check
Presidio's REGEX_TIMEOUT_SECONDS bounds one custom pattern on one text, so a
batch request could cost texts x patterns x that timeout (2,000 texts x 20
patterns x 2s) on a single handler thread. Charge every custom-pattern match to
a per-request 60s budget and reject the request with a 400 once it is spent.
…9581)

Browser tools that act on a page (navigate, click, type, submit, upload, ...)
run as the user in a browser carrying their signed-in sessions, but the
approval gate read only the catalog flag, which no browser tool declares, so
none of them ever prompted.

toolRequiresApproval now gates every browser tool that BROWSER_TOOL_OBSERVES_ONLY
does not mark observe-only, so a browser tool added later is gated by default,
and both the dispatch gate and the in-band lane read the same answer.
"Don't ask again" answers for browser tools are stored under one shared key,
so a single answer covers the browser instead of one prompt per kind of action.
…9583)

Select acceptance work from the task graph, separate docs site and platform checks, and preserve complete comparison history for push attribution.

Build docs once and run isolated acceptance groups in parallel after bounded server readiness. Reuse validated content and incremental build inputs with bounded retention.

Validate native source/toolchain fingerprints and restored output integrity, seed trusted branch caches, and balance desktop smoke across four whole-file shards. Bound organization-search diagnostics and reclaim closed-PR merge-ref caches.

Validation: full root tests, all workspace types, lint and 62 audits; real cache invalidation and Git attribution controls; complete hosted docs, integration, organization-search, Electron and packaged smoke coverage; final-head bot reviews.
…ity (#9578)

* feat(trigger): reach AWS from trigger.dev runs through workload identity

Trigger.dev runs reached S3, AppConfig, CloudWatch and hosted Bedrock with
the app IAM user's static keys. With TRIGGER_AWS_ROLE_ARN set, Sim's own AWS
clients now use temporary credentials for that role, exchanged for the run's
Trigger.dev workload-identity token (SDK 4.8.3). One session serves the
worker process, refreshed at each attempt's start so the post-run metrics
flush keeps valid credentials. Static keys and the default chain are
unchanged for ECS and self-hosted deployments.

* chore(deps): allow trigger.dev 4.8.3 ahead of the release-age window

4.8.3 is the first release with workload identity. The exclusion is
temporary and comes out once it clears the 7-day threshold on 2026-11-06.
Hosted-key metrics now key off Sim Cloud itself instead of the presence of
static AWS keys, so removing those keys never silences them.

* refactor(trigger): run the per-attempt setup through one module

The init hook loads app modules by relative path, which the source-convention
ratchet caps. Warming Redis and refreshing workload-identity credentials now
sit behind prepareTriggerAttempt, so init still imports one module.

* fix(aws): keep the default chain outside workload identity and cover ses

getAwsCredentials returned explicit static keys when present, which dropped
AWS_SESSION_TOKEN for clients that used to reach it through the default chain
(Bedrock, CloudWatch). It now returns the workload-identity provider or
undefined, so every non-Trigger path resolves exactly as before. The SES email
client reads it too, so worker email keeps working once static keys leave
Trigger.dev. The release-age comment records why the exclusions are bare names.

* test(uploads): give the presigner credentials where the client now reads them

The S3 client resolves credentials through the SDK default chain, which reads
process.env, so the presigner test stubs the keys there instead of the env
module mock.

* fix(aws): serve unexpired workload credentials when an exchange fails

Outside an executing attempt no workload-identity token can be minted, so a
post-run metrics flush near the refresh window failed even though the cached
credentials were still valid. A failed exchange now keeps serving the cached
credentials until a minute before they expire.
…ilities (#9582)

The permission gate decided by catalog lookup alone, and sim_cli has no catalog entry, so no CLI command was ever held. The worker exposes deploy, promote, run and cancel only through the CLI, so those catalog approval flags guarded nothing, and the CLI's own confirmation is answered by --yes in model argv.

Resolve the argv with the CLI's own parser to the command it would run and the v2 operations it declares, and hold the call when one is the capability of an approval-gated catalog tool. Both lanes ask the same predicate. Always-allow covers a CLI call only when its row names the command the parser resolves.
… each profile (#9585)

* fix(compare): keep Cowork, Make, and Langflow facts consistent within each profile

* fix(compare): carry the local-task exception into the Cowork alerting summary

* fix(compare): use Anthropic's local files or apps wording for every Cowork exception

* fix(compare): carry the local-task exception into the Cowork trigger summary
…ovider (#9586)

* fix(sso): mark email verified on sign-in through a verified-domain provider

* fix(sso): only verify the email the domain check approved
…#9587)

* improvement(sandbox): log per-phase timing for each sandbox execution

* test(sandbox): time the unchanged-CLI warm path on a controlled clock
…ed search's limit (#9590)

* improvement(search): page Slack Real-time Search to fill a Slack-scoped search's limit

* fix(search): keep earlier Slack pages on any later-page failure and page past short pages
…ile values models send (#9589)

* improvement(cli): accept the file references, list shapes and multi-file values models send

files batch-delete answers to --ids and --paths, reads a JSON array or a comma-joined run of ids as its entries, and the v2 bulk delete resolves files/ VFS paths through the canonical resolver after authorization, refusing the whole batch when one path names no single live file. tables rows batch-delete answers to --row-ids and the log reads to --workflow-ids, the request fields behind their shortened flags.

Workflow edits store a multi-file upload field as the list of file objects the editor writes, and an empty list clears it. A document page selection that lists separate pages says to read one range per call.

* refactor(cli): share the path predicate and other-spellings list

* test(files): drop unit cases the bulk-delete integration suite proves

* test(cli): use a flag batch-delete still refuses as the guessed-flag example

* fix(cli): refuse an empty entry in a joined file-reference list

* improvement(cli): name the metadata command when an embedded download refuses binary output
#9588)

* improvement(mothership): let a code call run for up to fifteen minutes

A render or build that needs more than five minutes was cut off at the
ceiling and failed, and the model started over in smaller pieces or
escaped the call with a background process the paused workbench then
stalled. Mothership code calls now run up to 900 seconds. The worker's
schema still decides what a model may request, so this is inert until
the worker raises its own maximum.

* fix(mothership): renew a running code call's sandbox CLI scope past its expiry

* fix(mothership): keep a long code call's upload bindings for its whole lifetime
… Sim's account (#9593)

* improvement(media): record a hosted-key auth failure when fal refuses Sim's account

Mothership's generate_audio and generate_video call fal on Sim's hosted keys
outside the tool executor, so a refused account (revoked key, or a lock for an
exhausted balance) only reached logs. The submit path now records a
Sim/HostedKey Failed point with reason auth on a 401, 402 or 403, the same
series the hosted-key dashboard already plots for executor tools, so an
alarm on that reason covers fal too.

* improvement(media): label fal auth failures with the refused key and use the shared env-flags mock

getFalApiKey now returns the env var its key came from, so a refused key in the
numbered pool is named on the metric. The metrics module reads isHosted on each
call instead of copying it at import, and the test sets it through the central
env-flags mock under fake timers.
@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
docs Ready Ready Preview Nov 3, 2026 2:56am UTC

Request Review

@waleedlatif1
waleedlatif1 requested a review from a team as a code owner November 3, 2026 02:51
@greptile-apps

greptile-apps Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] No blocking issue was established in the inspected changes.

Summary

Syncs dev with staging across desktop browsing, the browser extension, Assistant actions, documentation, file delivery, billing, and database migrations.

  • Organization members can manage Sim Links and open their saved destinations.
  • Assistant account actions wait for a user’s review before they proceed.
  • The Chrome extension sends page context to Sim chat from its side panel.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Assistant proposes an account action] --> B[Check account and current permissions]
  B --> C{Review required?}
  C -->|Yes| D[User reviews and edits]
  D --> E[Save decision and approved arguments]
  C -->|No| F[Claim execution]
  E --> F
  F --> G[Check exact admitted call]
  G --> H[Call provider]
Loading

Reviews (1) · Last reviewed commit: "Merge remote-tracking branch 'origin/sta..." · Reviewed by Greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

8 issues found across 1691 files

Confidence score: 2/5

  • openInBrowserProfile can launch an external browser for continue-sign-in even when prompts are disabled, bypassing the renderer’s opt-in. Block the launch when prompts are disabled.
  • readPage strips the trailing slash required by the catalog, so non-root HTTP and MCP page lookups return 404. Preserve the slash before calling getPage.
  • The docs.yml condition skips reusable calls from ci.yml, so main-branch docs changes no longer publish or verify the search index. Allow the workflow_call path while keeping the manual publish gate.
  • The If-None-Match path in apps/sim/app/api/files/utils.ts returns 304 before the final grant/provenance check and delivery audit. Revalidate access before allowing cached bytes to be reused.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/docs/components/ui/scroll-region.tsx">

<violation number="1" location="apps/docs/components/ui/scroll-region.tsx:22">
P2: Focusability is only recomputed when the wrapper or its first child resizes. Overflow that changes inside a block child, such as after a font swap or a content update, leaves `tabIndex` stale, and keyboard users cannot reach the scroll region. Re-check overflow on content changes too, for example with a MutationObserver on the subtree, or observe every child.</violation>
</file>

<file name="apps/docs/openapi-v2-files-audit.json">

<violation number="1" location="apps/docs/openapi-v2-files-audit.json:2154">
P2: The `fileId` description now accepts a VFS path such as `files/<folder>/<name>`, but the path template puts it in the URL. Literal slashes would split the route, so callers must percent-encode the value as `files%2F<folder>%2F<name>`. Add that note to the parameter description.</violation>
</file>

<file name=".github/workflows/docs.yml">

<violation number="1" location=".github/workflows/docs.yml:43">
P1: This condition skips every reusable invocation from `ci.yml`, so main-branch docs changes no longer publish or verify the search index. Allow the `workflow_call` path while retaining the manual `publish` gate for direct dispatches.</violation>
</file>

<file name="apps/desktop/scripts/build.ts">

<violation number="1" location="apps/desktop/scripts/build.ts:103">
P2: This build now consumes `packages/auth-ui` assets, but changes under that package do not trigger the desktop E2E workflow. Add `packages/auth-ui/**` to the desktop workflow paths and include the asset path in the desktop build inputs so renderer changes are validated and invalidate cached builds.</violation>
</file>

<file name="apps/docs/lib/http/pages.ts">

<violation number="1" location="apps/docs/lib/http/pages.ts:57">
P1: `readPage` removes the catalog's canonical trailing slash, so non-root HTTP and MCP page lookups return 404. Normalize non-root paths by adding the slash before calling `getPage`.

(Based on your team's feedback about canonical documentation page paths.) [a0b91456-4988-4a21-80c5-8bd0b634a1bb]</violation>
</file>

<file name="apps/desktop/src/main/browser-import/browser-launch.ts">

<violation number="1" location="apps/desktop/src/main/browser-import/browser-launch.ts:20">
P1: `openInBrowserProfile` launches an external browser without enforcing the renderer's `promptsEnabled` opt-in. Refuse `continue-sign-in` before this call when prompts are disabled; user activation alone is not the required prompt consent.</violation>
</file>

<file name="apps/sim/app/api/files/utils.ts">

<violation number="1" location="apps/sim/app/api/files/utils.ts:255">
P2: Matching `If-None-Match` returns 304 before the public stream's final grant/provenance check and delivery audit. Revalidate before allowing the browser to reuse cached bytes, or provide a separate validation callback for this branch.</violation>
</file>

<file name="apps/extension/src/extract.ts">

<violation number="1" location="apps/extension/src/extract.ts:62">
P3: `extractPageText` traverses unselected sibling chains before checking the selection range, so large DOM tails consume the capture budget and falsely mark complete selections as truncated. Queue a sibling only when it intersects `range`.</violation>
</file>

Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.

Turn on auto-fix | Re-trigger cubic

runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
timeout-minutes: 30
if: github.ref == 'refs/heads/main'
if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' && inputs.publish

@cubic-dev-ai cubic-dev-ai Bot Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: This condition skips every reusable invocation from ci.yml, so main-branch docs changes no longer publish or verify the search index. Allow the workflow_call path while retaining the manual publish gate for direct dispatches.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/docs.yml, line 43:

<comment>This condition skips every reusable invocation from `ci.yml`, so main-branch docs changes no longer publish or verify the search index. Allow the `workflow_call` path while retaining the manual `publish` gate for direct dispatches.</comment>

<file context>
@@ -18,23 +40,66 @@ jobs:
     runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
     timeout-minutes: 30
-    if: github.ref == 'refs/heads/main'
+    if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' && inputs.publish
+    concurrency:
+      group: docs-search-index-production
</file context>
Suggested change
if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' && inputs.publish
if: github.ref == 'refs/heads/main' && (github.event_name != 'workflow_dispatch' || inputs.publish)
Fix with cubic

/** Catalog lookup is the entire read boundary: no caller-supplied URL is fetched. */
export function readPage(path: string) {
const parsed = pagePathSchema.parse(path)
const canonical = parsed === '/' ? parsed : parsed.replace(/\/$/, '')

@cubic-dev-ai cubic-dev-ai Bot Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: readPage removes the catalog's canonical trailing slash, so non-root HTTP and MCP page lookups return 404. Normalize non-root paths by adding the slash before calling getPage.

(Based on your team's feedback about canonical documentation page paths.) [a0b91456-4988-4a21-80c5-8bd0b634a1bb]

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/docs/lib/http/pages.ts, line 57:

<comment>`readPage` removes the catalog's canonical trailing slash, so non-root HTTP and MCP page lookups return 404. Normalize non-root paths by adding the slash before calling `getPage`.

(Based on your team's feedback about canonical documentation page paths.) [a0b91456-4988-4a21-80c5-8bd0b634a1bb]</comment>

<file context>
@@ -0,0 +1,61 @@
+/** Catalog lookup is the entire read boundary: no caller-supplied URL is fetched. */
+export function readPage(path: string) {
+  const parsed = pagePathSchema.parse(path)
+  const canonical = parsed === '/' ? parsed : parsed.replace(/\/$/, '')
+  const page = getPage(canonical)
+  if (!page || page.url === '/') return null
</file context>
Fix with cubic

profileDirectory: string,
url: string
): Promise<void> {
await execFileAsync(

@cubic-dev-ai cubic-dev-ai Bot Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: openInBrowserProfile launches an external browser without enforcing the renderer's promptsEnabled opt-in. Refuse continue-sign-in before this call when prompts are disabled; user activation alone is not the required prompt consent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/desktop/src/main/browser-import/browser-launch.ts, line 20:

<comment>`openInBrowserProfile` launches an external browser without enforcing the renderer's `promptsEnabled` opt-in. Refuse `continue-sign-in` before this call when prompts are disabled; user activation alone is not the required prompt consent.</comment>

<file context>
@@ -0,0 +1,25 @@
+  profileDirectory: string,
+  url: string
+): Promise<void> {
+  await execFileAsync(
+    '/usr/bin/open',
+    ['-n', '-b', bundleId, '--args', `--profile-directory=${profileDirectory}`, url],
</file context>
Fix with cubic

}
const observer = new ResizeObserver(updateFocusability)
observer.observe(element)
if (element.firstElementChild) observer.observe(element.firstElementChild)

@cubic-dev-ai cubic-dev-ai Bot Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Focusability is only recomputed when the wrapper or its first child resizes. Overflow that changes inside a block child, such as after a font swap or a content update, leaves tabIndex stale, and keyboard users cannot reach the scroll region. Re-check overflow on content changes too, for example with a MutationObserver on the subtree, or observe every child.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/docs/components/ui/scroll-region.tsx, line 22:

<comment>Focusability is only recomputed when the wrapper or its first child resizes. Overflow that changes inside a block child, such as after a font swap or a content update, leaves `tabIndex` stale, and keyboard users cannot reach the scroll region. Re-check overflow on content changes too, for example with a MutationObserver on the subtree, or observe every child.</comment>

<file context>
@@ -0,0 +1,40 @@
+    }
+    const observer = new ResizeObserver(updateFocusability)
+    observer.observe(element)
+    if (element.firstElementChild) observer.observe(element.firstElementChild)
+    return () => observer.disconnect()
+  }, [])
</file context>
Fix with cubic

@@ -2151,13 +2151,12 @@
"name": "fileId",
"in": "path",
"required": true,
"description": "File identifier.",
"description": "File identifier, or the VFS path of a live file: `files/<folder>/<name>`. Pass the id when two files in a folder have names that display alike.",

@cubic-dev-ai cubic-dev-ai Bot Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The fileId description now accepts a VFS path such as files/<folder>/<name>, but the path template puts it in the URL. Literal slashes would split the route, so callers must percent-encode the value as files%2F<folder>%2F<name>. Add that note to the parameter description.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/docs/openapi-v2-files-audit.json, line 2154:

<comment>The `fileId` description now accepts a VFS path such as `files/<folder>/<name>`, but the path template puts it in the URL. Literal slashes would split the route, so callers must percent-encode the value as `files%2F<folder>%2F<name>`. Add that note to the parameter description.</comment>

<file context>
@@ -2151,13 +2151,12 @@
             "in": "path",
             "required": true,
-            "description": "File identifier.",
+            "description": "File identifier, or the VFS path of a live file: `files/<folder>/<name>`. Pass the id when two files in a folder have names that display alike.",
             "schema": {
               "type": "string",
</file context>
Suggested change
"description": "File identifier, or the VFS path of a live file: `files/<folder>/<name>`. Pass the id when two files in a folder have names that display alike.",
"description": "File identifier, or the VFS path of a live file: `files/<folder>/<name>` (percent-encode the slashes, e.g. `files%2F<folder>%2F<name>`). Pass the id when two files in a folder have names that display alike.",
Fix with cubic

})
mkdirSync('dist/renderer', { recursive: true })
for (const font of ['geist-variable.woff2', 'geist-mono-variable.woff2']) {
cpSync(resolve('../../packages/auth-ui/src/assets', font), join('dist/renderer', font))

@cubic-dev-ai cubic-dev-ai Bot Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: This build now consumes packages/auth-ui assets, but changes under that package do not trigger the desktop E2E workflow. Add packages/auth-ui/** to the desktop workflow paths and include the asset path in the desktop build inputs so renderer changes are validated and invalidate cached builds.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/desktop/scripts/build.ts, line 103:

<comment>This build now consumes `packages/auth-ui` assets, but changes under that package do not trigger the desktop E2E workflow. Add `packages/auth-ui/**` to the desktop workflow paths and include the asset path in the desktop build inputs so renderer changes are validated and invalidate cached builds.</comment>

<file context>
@@ -128,8 +95,13 @@ const renderer: BuildOptions = {
+  })
+  mkdirSync('dist/renderer', { recursive: true })
+  for (const font of ['geist-variable.woff2', 'geist-mono-variable.woff2']) {
+    cpSync(resolve('../../packages/auth-ui/src/assets', font), join('dist/renderer', font))
+  }
   if (watch) {
</file context>
Fix with cubic

headers.set('Accept-Ranges', 'bytes')
if (file.etag) headers.set('ETag', file.etag)

if (file.etag && ifNoneMatchHolds(requestHeaders.get('if-none-match'), file.etag)) {

@cubic-dev-ai cubic-dev-ai Bot Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Matching If-None-Match returns 304 before the public stream's final grant/provenance check and delivery audit. Revalidate before allowing the browser to reuse cached bytes, or provide a separate validation callback for this branch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/app/api/files/utils.ts, line 255:

<comment>Matching `If-None-Match` returns 304 before the public stream's final grant/provenance check and delivery audit. Revalidate before allowing the browser to reuse cached bytes, or provide a separate validation callback for this branch.</comment>

<file context>
@@ -218,6 +220,75 @@ export function createConditionalFileResponse(
+  headers.set('Accept-Ranges', 'bytes')
+  if (file.etag) headers.set('ETag', file.etag)
+
+  if (file.etag && ifNoneMatchHolds(requestHeaders.get('if-none-match'), file.etag)) {
+    return new NextResponse(null, { status: 304, headers })
+  }
</file context>
Fix with cubic

}
const node = pending.pop()
if (!node) break
if (node !== root && node.nextSibling) pending.push(node.nextSibling)

@cubic-dev-ai cubic-dev-ai Bot Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: extractPageText traverses unselected sibling chains before checking the selection range, so large DOM tails consume the capture budget and falsely mark complete selections as truncated. Queue a sibling only when it intersects range.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/extension/src/extract.ts, line 62:

<comment>`extractPageText` traverses unselected sibling chains before checking the selection range, so large DOM tails consume the capture budget and falsely mark complete selections as truncated. Queue a sibling only when it intersects `range`.</comment>

<file context>
@@ -0,0 +1,113 @@
+    }
+    const node = pending.pop()
+    if (!node) break
+    if (node !== root && node.nextSibling) pending.push(node.nextSibling)
+    if (range && !range.intersectsNode(node)) continue
+    if (node.nodeType === Node.ELEMENT_NODE && hidden(node as Element)) continue
</file context>
Suggested change
if (node !== root && node.nextSibling) pending.push(node.nextSibling)
if (
node !== root &&
node.nextSibling &&
(!range || range.intersectsNode(node.nextSibling))
)
pending.push(node.nextSibling)
Fix with cubic

@waleedlatif1
waleedlatif1 merged commit 8f6a420 into dev Nov 3, 2026
93 checks passed
@waleedlatif1
waleedlatif1 deleted the chore/sync-dev-with-staging-2026-11-03 branch November 3, 2026 03:43

This branch was successfully deployed

1 active deployment
Preview — bd0ec1f5 Deployed Nov 3, 2026 by vercel[bot]
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