v0.7.54: resource sorting, deployed chat improvements, self hosting docs overhaul, nextjs and bun upgrade - #6237
Conversation
…s reach the top (#6213) * fix(resources): stop hoisting folders so pinned items reach the top Folders and their sibling resources were sorted as two lists and concatenated folders-first, so a pinned file, table, or knowledge base could never rank above an unpinned folder — pinning only reordered within each partition. Sort them as one list instead: pinned -> sort key -> name, in a shared `sortResources` comparator. Rows with no value for the active column (a folder has no row count or token count) sort last in both directions, and the name tiebreak is never inverted by `desc`. Also aligns the surfaces that browse the same resources: the mothership resource trees, the collapsed sidebar flyout (whose nested levels partitioned folders-first while its own root interleaved), the sidebar file tree, and the recently-deleted tiebreak. Files' sort params were nullable only to encode "folders name/asc, files updated/desc"; with one list that state is gone, so they default to updated/desc like Tables and Knowledge. * improvement(resources): prefetch pinned ids and members with the resource lists Files, Tables, and Knowledge already prefetched their items and folders, but not the two lists a complete row needs. Pinned ids are the list's primary sort key now, so a page that painted before they arrived rendered the whole list in the wrong order and then visibly re-sorted. Members back the Owner column, which painted empty and filled in after. Both now hydrate alongside the lists via `prefetchResourceListChrome`. Pinned keys move to `hooks/queries/utils/pinned-item-keys` so the server prefetch can address them without pulling the contracts barrel and the optimistic-mutation machinery into the route. * chore(knowledge): drop the dead pre-redesign knowledge base sort `sortKnowledgeBases` was a second, complete ordering for knowledge bases (name/createdAt/updatedAt/docCount) left over from the old sort dropdown, with no consumers since the shared Resource sort menu replaced it — the kind of duplicate that silently diverges from the page it shadows. Its `SortOption`/`SortOrder` types and the `SORT_OPTIONS` list that fed it were dead with it. `utils/sort.ts` now holds only a filter, so it is renamed to `utils/filter.ts`. * fix(resources): sort unknown owners last and align the knowledge sort menu An owner id that resolves to no workspace member renders an empty cell, but its sort key was `''`, so those rows floated to the TOP of an ascending Owner sort while every other valueless cell sorted last. They now key `null` and follow the same nulls-last rule on all three pages. The Knowledge sort menu also listed Owner after Last Updated while its column order — and Tables' menu — put Owner before it. * chore(knowledge): delete the dead base-card grid view `BaseCard`/`BaseCardSkeleton`/`BaseCardSkeletonGrid` were the pre-redesign grid-card knowledge base view, kept alive only by the barrel re-export — no call sites since the Resource list replaced it. Removing it orphans `components/constants.ts` entirely (its sort types went with the dead sort; its three class-name constants had no consumers left), so that goes too.
…ars (#6211) * fix(tooltip): remove velocity skew/scale that blurred text on every appear The tooltip animated a fractional scale() + skew() over 150ms on the element containing its text. Chrome promotes the bubble to a compositor layer for the transition, rasterizes the text once at the pre-transition scale, then GPU-resamples that bitmap for the duration — so text rendered blurry until the transition settled and the layer re-rasterized at 1:1. It fired on every appear: a pointer entering a trigger is by definition moving, so the first pointermove after pointerenter always set a non-zero skew and a fractional scale. - drop the velocity-reactive skew/scale flourish and the pointer-velocity bookkeeping that existed only to feed it - round tooltip position to whole pixels; clientX/clientY are fractional on HiDPI/zoomed displays, leaving the bubble on a subpixel boundary - drop the dead `filter` from the transition list — nothing ever set a filter - skip the state update when the rounded position is unchanged, so pointer jitter no longer re-renders every Tooltip.Trigger/Content consumer The 150ms ease-out translate is kept, so the bubble still trails the cursor. * improvement(tooltip): keep the velocity flourish, drive it without a CSS transition Restores the velocity-reactive skew/scale removed in the previous commit. The flourish was never the problem on its own — handing it to a CSS transition was. An interpolated fractional scale makes the compositor rasterize the tooltip's text once and resample that bitmap for the duration, which is what read as blur. Applied as a static value per pointer event instead, so every frame is rasterized at its own scale: - split the transform across the individual `translate`, `scale`, and `transform: skew()` properties, and transition only `translate` — position still eases toward the cursor, the flourish no longer interpolates - smooth the pointer velocity in JS (low-pass filter) to replace the smoothing the CSS transition used to provide, so the squish still ramps rather than snapping between raw per-event velocities - quantize the flourish to 3 decimals so jitter below the visible threshold settles instead of re-rendering every consumer Whole-pixel position rounding and the redundant-update bail-out are unchanged. * fix(tooltip): don't seed pointer velocity from the trigger box on focus The previous commit routed `onFocus` through a shared reveal helper that seeds `lastPointerRef` from the coordinates it is given. For focus those are the trigger's box center, not the pointer — so if the pointer already happened to be over the trigger, the next `pointermove` measured the box-to-cursor delta as velocity and spiked the skew/scale flourish. Split the helper in two: reveal-from-pointer seeds velocity tracking, reveal-from-element leaves it cleared. Restores the pre-PR behavior, where focus explicitly nulled the pointer snapshot. Caught by Cursor Bugbot. * fix(tooltip): make the flourish smoothing frame-rate independent The velocity low-pass filter applied a fixed coefficient per pointer event, so how fast the squish settled depended on how fast the device emitted events — 233ms at 30Hz down to 29ms at 240Hz, an 8x spread for the same gesture. It was also far snappier than the 150ms CSS ease-out it replaced, so the flourish read as twitchier than before. Derive the coefficient from the real elapsed time instead (1 - exp(-dt / tau), tau = 50ms). Settling is now flat at ~150ms from 60Hz upward, matching the duration of the transition this stands in for. Also separates the smoothing delta from the velocity-normalization delta: the latter is still floored at one frame to keep a 1ms event from reporting an enormous velocity, but flooring the former was itself a source of frame-rate dependence below 16ms. Verified against Chrome's documented re-raster behavior: a layer is re-rastered at its new scale when the scale changes via script, but not when a declarative animation interpolates it, which is why the flourish must stay out of the transition list. https://developer.chrome.com/blog/re-rastering-composite
* fix(security): meter and throttle the deployed-chat TTS relay POST /api/proxy/tts/stream treated "a live public chat exists" as authorization to spend the platform ElevenLabs key. A public chat id is handed to every visitor, so any anonymous caller could synthesize speech with no length cap, no rate limit and no usage accounting. Bring the relay in line with its STT sibling (/api/speech/token): - Resolve the chat's workspace and bill synthesized characters to that payer via a new `voice-output` usage source, so spend is attributable and counts against the plan's usage limit (402 once exceeded). - Throttle per IP before any database work, and per chat afterwards, to bound both one caller hammering many chats and many callers hammering one chat. - Cap `text` at 2000 characters and allowlist `voiceId`/`modelId`, so the caller can no longer choose an unbounded charge, a premium or cloned voice, or the billing model. - Drop `Access-Control-Allow-Origin: *`, which let any third-party page read the audio; deployed chat and the Office embed are same-origin. * fix(security): correct TTS metering, pricing and body cap Follow-up review of the previous commit found five defects in it: - Usage rows collided. `usage_log.event_key` is unique and inserts are conflict-do-nothing, and the key is derived from the entry's stable fields. With no explicit sourceReference, two synthesis calls of equal character count in the same workspace produced the same key, so every repeat length went unbilled — defeating the metering this change is for. Each call now carries a unique sourceReference. - Priced at $0.10 per 1k characters, twice the published ElevenLabs Flash/Turbo rate of $0.05, which would have overcharged customers 2x. - No body cap, so an anonymous caller could make the route buffer up to the shared 50 MB default before validation. Now 16 KB, as the STT sibling does. - Threshold settlement ran per sentence: several queries and a possible Stripe call on a realtime path. The workflow execution that produced the text already settles the payer. - The per-IP bucket was described as preventing database amplification. getClientIp trusts the leftmost X-Forwarded-For, so an attacker rotates past it; the comment now says the per-chat bucket is load-bearing. * refactor(chat): share the deployed-chat auth gate across voice routes Review of the previous commits surfaced duplication and one more gap: - The TTS and STT routes had grown near-identical copies of the chat auth + payer lookup. Extracted to resolveDeployedChatCaller, so the gate and the payer resolve together and cannot drift per route — that duplication is how the unmetered TTS path shipped in the first place. - Neither copy filtered chat.archivedAt, so an archived chat could still authorize spend against its former owner's workspace. The shared lookup now filters it, fixing both routes at once. Note: not covered by a test — the db chain mock does not evaluate WHERE clauses, so an assertion here could not fail. - Replaced the route's hand-rolled 429 builder with the existing enforceIpRateLimit helper, and added enforceChatRateLimit alongside the per-user/IP/workspace helpers. Gains the standard Retry-After and X-RateLimit-Reset headers plus throttle logging. - Dropped a test that asserted a module the route no longer imports was never called: it could not fail. - Narrowed the contract: unexported the single-use allowlists and dropped .passthrough() now that the body is a closed shape. * fix(security): fail closed when voice-output usage cannot be recorded Review round 1 findings: - A ledger write failure previously logged and streamed the audio anyway, leaving the spend unrecorded and the payer's usage understated. The caller is anonymous, so serving audio we could not charge for is the unmetered spend this route exists to prevent — it now returns 500. - Use generateId() from @sim/utils/id rather than crypto.randomUUID, per the AGENTS.md ID rule. generateId returns a full UUID v4, so the per-call uniqueness the usage_log event_key depends on is unchanged. * fix(chat): split long TTS text so the relay cap cannot drop audio The client sentence-splits on Western `.!?` only, so text that never matches — CJK punctuation, or a list with no terminal punctuation — accumulates and is flushed as one block at the end of the stream. Against the new 2000-character relay cap that block is rejected and the whole message plays no audio, a regression introduced by adding the cap. Split to cap-sized pieces at the single point that enqueues synthesis, so both the per-sentence path and the end-of-stream flush are covered. Prefers a whitespace or CJK punctuation boundary, falling back to a hard cut when a block has none. The server cap stays as the enforcement point. * fix(security): release the vendor stream when metering rejects the request The fail-closed branch returned 500 with the ElevenLabs response body still open, so synthesis and download kept consuming vendor and runtime resources for a caller that was already rejected. Cancel it before returning, and assert the cancellation in the test.
…6214) The hosted `<PublicEnvScript>` rendered a plain `<script>`, which lands at the end of `<head>` — after the ~40 `<script async>` chunk tags Next emits at the top of the document. An async script runs as soon as its fetch resolves, so on a warm cache a Next chunk could execute (and hydration begin) before the parser reached the env tag, leaving `window.__ENV` undefined for the first render. That surfaced as "Something went wrong" on the workflow page, since `getBaseUrl()` throws during the deploy modal's render, and as the socket falling back to the page origin instead of NEXT_PUBLIC_SOCKET_URL. Regressed in #5522, which replaced next-runtime-env's PublicEnvScript (to avoid its unstable_noStore forcing dynamic rendering) with a static equivalent that dropped the beforeInteractive strategy. - render the library's own `<EnvScript>`, which defaults to beforeInteractive and does not call unstable_noStore — hosted and self-hosted now share one implementation and one loading strategy - drop the hand-rolled serialization and `<` escaping; Next's beforeInteractive path already runs the payload through htmlEscapeJsonString, which escapes `& > < U+2028 U+2029` - fall back to the browser origin in getBaseUrl() rather than throwing, so a missing injected env can never tear down a page through the error boundary; server-side callers still fail loudly - read NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED via getEnv() in the SSO form, matching login/signup/auth-modal — `env.X` returns the build-time placeholder, not the runtime value
Removes the voice-first interface and TTS playback from the deployed chat, keeping workspace dictation, which is a separate feature. - Deletes the VoiceInterface UI and its particles canvas, the chat mic input, the TTS audio-streaming hook, the /api/proxy/tts/stream relay and its contract, and the voice-settings query hook. - Unpicks the voice wiring in chat.tsx and use-chat-streaming: the audio stream handler, sentence-splitting for speech, voice-first mode state, and the isVoiceInput plumbing through ChatInput. - Drops the now-dead chatId branch from /api/speech/token. It was the anonymous public-chat path; with no caller left it would have stayed an unauthenticated relay spending the platform key. That leaves resolveDeployedChatCaller unused, so it goes too. - Removes code the above orphaned: MAX_CHAT_SESSION_MS, the noop util, the audio/position refs in use-chat-streaming that were only ever written, and the chatId field on the speech contract. Keeps /api/settings/voice, lib/speech and use-speech-to-text: the workspace home input still uses them. Keeps the voice-output usage source, enum and label — Postgres cannot drop an enum value, and historical usage_log rows still need a label to render.
Full-file cleanup pass over the files voice mode was removed from. - /api/speech/token: with the chatId branch gone, billingAttribution is always resolved, so four `billingAttribution ? ... : ...` guards and the checkActorUsageLimits fallback were unreachable. Removed, along with the now-unused imports and test mock. - chat.tsx: the inputValue state had one remaining writer (`''`) and no reader — ChatInput has owned its own input value since it went uncontrolled, and removing the voice-transcript caller left handleSendMessage always receiving an explicit message. - use-chat-streaming: messageIdMap was written in three frame handlers and never read, and setIsStreamingResponse was returned but never destructured by the only consumer. - Comments: dropped JSX section labels that restate the element beneath them and TSDoc that restates the identifier; narrowed the speech contract's workspaceId doc, which existed to contrast with the removed chatId field.
* improvement(ci): move CodeQL off default setup onto Blacksmith Default setup pinned every scan to a 4-vCPU GitHub-hosted runner with no cancel-in-progress: PR scans ran 30-125 min and re-ran on every push (#6183 burned six overlapping runs). None of that is reachable from the settings UI, so the config moves into the repo. - Blacksmith 8-vCPU via the same CI_PROVIDER escape hatch as ci.yml - cancel-in-progress scoped to pull_request so push/schedule scans finish - push to main + PR to main/staging + nightly safety net - paths filter so doc-only PRs skip the run entirely - paths-ignore config drops tests/mocks/fixtures: 12,716 -> 11,128 files - languages: javascript-typescript + actions; python dropped (7 files in tree) Default setup has been disabled; the two cannot both be active. * fix(ci): restore CodeQL coverage of the data-drain test route Review round 1. - Drop the '**/test/**' and '**/tests/**' globs. A `test` directory is a routable Next.js path segment, and those globs excluded apps/sim/app/api/organizations/[id]/data-drains/[drainId]/test/route.ts — a POST handler that authorizes, decrypts destination credentials and makes an outbound request. CodeQL paths-ignore has no `!` negation to carve it back out, and the globs only covered 76 of 12,716 files. - Add `ready_for_review` to the pull_request activity types. It is not a default type, so a PR opened as a draft and later marked ready was skipped by the draft guard and never rescanned until the next push.
…6217) * fix(sso): derive the SSO callback URL during render `callbackUrl` was seeded into `useState('/workspace')` and overwritten from a `useEffect` that read `searchParams`, so the first painted frame always carried the default. On any deep link with `?callbackUrl=`, the "Sign in with email" and "Sign up" links briefly pointed at `/workspace` instead of the requested destination, and a click landing in that window navigated to the wrong place. - derive `callbackUrl` from `searchParams` during render; the validation gate is unchanged, so an off-origin or malformed value still falls back to `/workspace` - keep the warning for a rejected value in an effect, now keyed on the param itself rather than the `searchParams` object, so it fires once per actual change instead of once per identity change - add first-frame tests via `renderToString`, which runs no effects and so pins exactly the window the old code got wrong * fix(auth): resolve callback URLs against the app origin server-side `validateCallbackUrl` compared against a sentinel base (`https://callback-url-validator.invalid`) when `window` was undefined, so the server rejected every absolute URL — including the same-origin ones the function documents as valid. A component deriving a callback URL during render therefore produced one destination in the SSR markup and a different one after hydration. The exposure was not new to the SSO form: `login-form.tsx` and `signup-form.tsx` already derive their callback URL during render on `force-dynamic` pages, so both carried the same divergence. - resolve against the deployment's own origin server-side, so the server reaches the same verdict the browser will after hydration - fall back to the sentinel when the app URL is unset or unparseable, which keeps the server fail-closed: absolute URLs are rejected, as before - cover the absolute same-origin case and the unset-app-URL fallback in the existing suite; all 15 open-redirect rejection cases are unchanged * fix(env): drop the getBaseUrl browser-origin fallback The fallback was added in #6214 as a safety net while the real cause — the hosted env script losing its `beforeInteractive` strategy — was fixed in the same PR. With the injection ordering restored, `window.__ENV` is populated before hydration, so the fallback is unreachable in any correctly configured deployment. Guessing the origin was also unsafe in the one case it could still fire. An opaque origin — a sandboxed iframe, and `/chat/*` is deliberately embeddable — serializes to the string `'null'`, which is truthy, so `getBaseUrl()` would have returned `'null'` and every call site would have silently built `null/api/...`. A throw surfaces the misconfiguration instead of encoding it into request URLs. - restore the unconditional throw when NEXT_PUBLIC_APP_URL is unset or blank - mirror it back in the shared testing mock - flip the two fallback tests to assert the throw, keeping whitespace-only coverage Server-side behavior is unchanged: there was never a `window` to fall back to, so callers that already guard `getBaseUrl()` (`getBaseDomain`, `validateCallbackUrl`) keep their existing fail-closed paths.
PATCH /api/chat/manage/[id] calls performFullDeploy when the workflow has drifted from its active deployment, so editing a chat can mint a new deployment version. useUpdateChat invalidated only chatStatus and chatDetail, leaving the deployment panel showing the previous version and a stale "needs redeployment" indicator until the staleTime expired. Both mutations now route through invalidateDeploymentQueries, the shared helper the rest of the deployment surface uses. That also picks up deployedState, which useCreateChat's hand-rolled list had omitted even though performChatDeploy replaces the deployed workflow state. Tests cover both mutations and were verified to fail against the previous invalidation.
* refactor(chat): clean up the deployed chat surface Eight-angle cleanup pass over the full contents of the chat surface and the speech code that survived the voice-mode removal. Dead code - enforceChatRateLimit: added for the TTS relay in #6212, orphaned when #6215 deleted that route. Zero consumers. - ChatToolCallStatus, ChatErrorType, and six unused CHAT_ERROR_MESSAGES keys (only GENERIC_ERROR and CHAT_UNAVAILABLE are read). - scrollToMessage was declared and destructured by ChatMessageContainer but never used in its body; removing the prop also made the scrollToShowOnlyMessage branch unreachable, since the sole caller passed true. - permissionState and the language prop on useSpeechToText: both write-only across the repo. - The image branch in ChatFileDownload's renderIcon returned the same DefaultFileIcon at the same size as the fallback. - chatKeys.status/detail: aliases of deploymentKeys nothing imported, and misleading since they root under a different key namespace. Redundant state - password-auth and email-auth each kept a boolean in lockstep with `errors.length > 0`; email-auth also validated on every keystroke and then immediately hid the result. - file-download tracked hover in state to drive one opacity class; now group-hover. Verified emcn Button sets no `group` class of its own. Memoization - ChatMessageContainer's memo() could never bail: chat.tsx passes an inline arrow for scrollToBottom and displayMessages is a fresh array. Four of the five things that re-render ChatClient are its props anyway, so the memo is dropped rather than propped up. - ClientChatMessage keeps its memo — it blocks markdown re-parsing — but loses the custom comparator, which compared proxies (a key:status fingerprint, files by length) and ignored attachments and type entirely. Default shallow compare on its single prop is both simpler and stricter. - Six useCallbacks whose consumers are native DOM handlers or inline arrows, so nothing observed their identity. Effects - The scroll listener attached in an effect keyed on [chatConfig, authRequired] — values it never reads, standing in for "the container has mounted". It now attaches via a ref callback, so it no longer re-attaches on every config refetch. Design system and a11y - z-[100] -> z-[var(--z-dropdown)] (same value), shadow-lg -> shadow-medium, list styles from inline style to Tailwind classes, hover: -> hover-hover: on touch-reachable targets, Check sourced from emcn alongside its Duplicate pair. - Accessible names on the remove-attachment, stop, and send buttons, which announced only as "button". - Dropped a keyboard handler on a role='group' div with no tabIndex, where target === currentTarget was unreachable, and the Tooltip Provider wrappers and delayDuration, which emcn documents as no-op passthroughs. * fix(chat): restore markdown list markers The design-system pass swapped inline `listStyleType` for Tailwind classes, but the edit that added `list-disc`/`list-decimal` silently did not apply while the one removing the inline style did. With Preflight setting `list-style: none`, every bullet and number in an assistant response disappeared. `list-item` on the `li` sets display only, not the marker type.
…dd self-host settings, land setup on signup (#6216) * fix(auth): skip email verification when no mail provider is configured Signup pushed /verify unconditionally, stranding self-hosted deployments with no mail provider on a screen no email could ever satisfy. Derive one server-side effective value (verification enabled AND deliverable) and read it from Better Auth enforcement, signup routing, and the verify page. * feat(settings): add a self-host section with the managed Chat keys link Self-hosters had no in-app pointer to the managed service that issues their Chat keys. New Settings > System > Self-host section, gated on `requiresSelfHosted` so it is absent on hosted Sim, containing only that link. * improvement(setup): land the wizard handoff on signup A freshly provisioned deployment has no accounts and / renders the marketing landing page, so the bare origin left operators hunting for the CTA. Single-source the URLs and point every open-Sim handoff at /signup across all three modes. * improvement(settings): mark the self-host section with a sprout Server was already doing double duty for MCP servers and Mothership, and the icon set ships no botanical glyph, so the mark is a text emoji. * improvement(settings): draw the sprout as an emcn line icon, move to Platform The emoji rendered in the platform's own colors, so it was the one glyph in the nav that ignored --text-icon. Replaced with a hand-drawn emcn Sprout (24 grid, 1.55 stroke, currentColor) matching the house style, renamed the tab to Self hosting, and regrouped it under Platform — self-hosting is deployment-wide, not per-workspace. Still self-hosted-only. * improvement(settings): drop the section header from self hosting One row does not need a section label, and removing it takes the divider with it. The body is now the Chat keys row and its managed-keys link, nothing else.
* refactor(voice): load STT availability through React Query useSpeechToText fetched `/api/settings/voice` inside an effect and stored the result in useState behind a hand-rolled mountedRef guard: no cache, no dedupe across mounts, and no AbortSignal, so the response was fetched and parsed even after unmount. Two simultaneously mounted consumers issued two requests. It also bypassed hooks/queries/**, which is where every other server read in the app lives — and it escaped `check:react-query`, whose audit only covers useQuery/useMutation call sites. The value is server env read at request time, so it cannot change within a session; the new hook uses an infinite staleTime and a caller-controlled `enabled` so clients without the audio APIs never issue the request. Hydration is unchanged: SSR renders unavailable, and the first client render still resolves unavailable because `data` is undefined until the fetch settles. No initialData, deliberately — adding it would break that. mountedRef stays; it is still load-bearing for the streaming lifecycle. * test(queries): unmount rendered roots between tests renderHookWithClient created a React root per test but never tore it down, so trees stayed mounted with live QueryClient observers until worker teardown and async notifications could cross test boundaries. Audited every test in the repo using createRoot: 51 of 53 already unmount. The two that did not were both mine — voice.test.tsx here and chats.test.tsx from #6223 — so both are fixed and the pattern is now uniform. * fix(voice): let a failed STT probe recover on a later mount The app QueryClient sets retryOnMount: false and retry: 1, and refetchOnWindowFocus only refetches stale queries — which an infinite staleTime never becomes. So one transient failure cached the error for the life of the client and hid the mic until a full page reload. The effect this replaced refetched on every run, so retryOnMount: true restores parity: no refetch after success, a retry per mount after failure. Test asserts recovery under the app's real query defaults and fails without the override.
…nd deploy modals (#6226) * improvement(emcn): share one emails/domains chip input across share and deploy modals Extracts the emails chip lifecycle out of ChipModalField type='emails' into a standalone ChipEmailsInput, and points both the file share modal and the deploy modal's chat tab at it instead of their hand-rolled TagInput wiring. - add ChipEmailsInput (dedupe, normalize, format gate, paste, per-chip errors) with an allowDomains opt-in for bare @domain.tld entries - share modal and deploy modal chat tab now use it; drop both hand-rolled add/remove/validate implementations and the dead emailError state - move the shared allowlist policy into validateAllowlistEntry - drop the "Add specific emails or whole domains" hint text - give OutputSelect a size prop; the deploy modal chat tab uses the 30px chip trigger so it lines up with the Title field above it - drop overflow-y-auto from the chat deploy form, which was promoting overflow-x to auto and rendering a stray horizontal scrollbar * improvement(utils): one email syntax gate, drop the backtracking placeholder regex Audit follow-ups on the emails chip input. - move EMAIL_SYNTAX_REGEX and the new @Domain pattern into @sim/utils/string as isValidEmailSyntax, so emcn and lib/messaging/email/validation.ts stop keeping byte-identical copies of the RFC 5322 regex - allow single-label domains (@intranet) again — requiring a dot rejected entries the old startsWith('@') check accepted, which self-hosted deployments use. A lone @ and malformed labels stay rejected - replace derivePlaceholderWithTags' /^Enter\s+(.+?)s?$/i with string ops; CodeQL flagged it as polynomial backtracking (js/redos). Verified identical output across the placeholder shapes in use - forward the emails control's props explicitly instead of underscore-discard destructuring, matching ChipModalFileControl in the same file * test(email): pin the allowlist entry rules Covers isValidEmailSyntax's allowDomains branch (single-label domains stay valid, malformed bare domains that the old startsWith('@') check accepted do not), the 254-character cap, the DNS label limit, and validateAllowlistEntry waiving address-level policy for bare domains. Verified both new rules fail when the behavior is reverted.
…ting docs (#6225) * feat(self-host): align Docker Compose with Helm and overhaul self-hosting docs Docker Compose shipped no scheduler, so scheduled workflows, every polling trigger, connector syncs, the outbox, and data drains silently never ran. Adds a cron service running the same 18 jobs the Helm chart schedules as CronJobs, and closes the remaining behavioral gaps between the two paths: bundled Redis in the chart, no hosted plan caps in chart defaults, pinned image tags, and fail-fast secrets. A CI check keeps the schedulers in sync. Also rewrites the self-hosting docs: 14 new pages, 8 updated, reorganized into Install / Configure / Operate. * fix(self-host): drop bun install from chart CI, remove air-gapped and backup docs The scheduler-parity check pulled a full dependency install into the chart-validation job, which fails building isolated-vm on that runner. Rewritten to use only node builtins so the job installs nothing. Also removes the air-gapped and backup/restore pages, and stops pinning a concrete release in the docs so the examples do not go stale each release. * fix(helm): bundle Redis in secret-manager modes unless the URL is supplied Suppressing Redis whenever a secret mode was active left those deployments with no Redis at all — REDIS_URL is optional there and both shipped examples omit it. The chart now steps aside only on a detectable signal: an explicit app.env.REDIS_URL, an ESO remoteRefs.app.REDIS_URL mapping, or the new redis.provideUrl=false opt-out for a pre-created Secret it cannot read. * fix(compose): derive realtime BETTER_AUTH_URL from NEXT_PUBLIC_APP_URL realtime read BETTER_AUTH_URL directly and fell back to localhost while simstudio derived it from NEXT_PUBLIC_APP_URL, so setting only the public origin left realtime authenticating against http://localhost:3000. * fix(helm): deliver bundled REDIS_URL via ConfigMap so an operator value always wins Injecting REDIS_URL as an inline container env made it beat every envFrom source, so a REDIS_URL held in a pre-created Secret or synced by External Secrets was silently shadowed and traffic moved to a fresh in-cluster Redis. Kubernetes resolves duplicate envFrom keys by letting the last source win, so the bundled URL now ships as a ConfigMap listed before the app Secret. Any operator-supplied value overrides it without the chart needing to read it, which also removes the redis.provideUrl flag the previous attempt required. * docs(helm): spell out the egress rule external datastores need The default NetworkPolicy allows 443 plus the bundled Postgres and Redis by pod selector. Anything you run outside the chart on another port needs its own rule, which is easiest to miss when REDIS_URL arrives via a Secret the chart cannot inspect. Adds a copyable example to the production checklist and the security guide. * feat(helm): add networkPolicy.allowExternalEgress for managed datastores The default policy allows 443 plus the bundled Postgres and Redis by pod selector, so a managed datastore on another port needs a hand-written CIDR rule — awkward when REDIS_URL arrives via a Secret the chart cannot inspect. Adds an opt-in switch that drops the port restriction while still blocking the cloud metadata endpoints. Defaults to false, keeping this chart stricter than the common chart default of unrestricted egress.
…terminal features (#6196) * icon styling * feat(desktop): isolate chat browser and terminal sessions * feat(desktop): uncap browser and terminal tabs * feat(desktop): polish browser and terminal resources * improvement desktop * fixes * updates * fixes * fix * update tests
…6229) The Redis byte-budget branch in doFlush requeued the rejected batch and rethrew, skipping the MAX_PENDING_EVENTS trim every other failure path applies. The backlog then grew for the rest of the run and each retry re-serialized it, so a wide parallel fan-out could drive unbounded heap growth and stall the event loop. Drop rejected chunks instead of requeueing, pace retries through the existing backoff, and split batches that exceed the single-write cap so an oversized batch can make progress instead of stalling forever. Terminal status is now writer-scoped, since a concurrent scheduled flush can be the loop that drains the final chunk, and a terminal event whose batch was dropped is retried on its own rather than lost with it. Record terminal stream meta when the terminal event cannot be buffered, so reconnecting readers stop polling an active stream until their deadline. Drop the unused reserve/release budget helpers.
…6231) * fix(execution): offload buffered event values under budget pressure An execution buffers EVENT_LIMIT events inside a per-execution byte budget, so a full ring only fits if events average under budget/EVENT_LIMIT. Values were only offloaded to object storage at the shared 8 MiB cap, far above that, so a run emitting large block outputs exhausted its budget within a few dozen events and stayed pinned at its ceiling for the rest of its life. Applying that ceiling to every run would be worse than the problem: the SSE stream carries the compacted event and the terminal renders a ref only as a preview, so ordinary block outputs would stop being readable live, and every value would cost an object-storage write on the hot path. Engage the tight ceiling only once a run has actually buffered past half its budget. A short run keeps full-fidelity output and pays nothing; a runaway one stops accumulating. Both bounds derive from the existing budget rather than being asserted, and preserved UserFile base64 is exempt — it is an explicit request for inline delivery, already bounded by its own cap and the strip-and-recompact fallback. Also stop a failed resume-path buffer write from failing the run: it was awaited bare, so the rejection propagated into the executor callback and failed work that had already completed. The buffer only backs reconnect replay, so degrade to live-only delivery the way the execute route does. * fix(execution): measure pressure at write time and keep terminal status last Pressure was read from bytes counted once a flush succeeded, but a burst is compacted long before the scheduled flush runs — so the very batch that exhausts the budget went through at the loose ceiling and was dropped instead of offloaded. Count bytes as each event is compacted. Separately, the terminal-alone retry stamped terminal status while entries queued ahead of it were still unwritten. Terminal status is the reader's end-of-run signal: a reconnecting client drains what is in Redis and closes, so those entries were stranded behind a stream it had already finished with. Drain the backlog first, then publish the terminal event. * fix(execution): do not lose the backlog or publish terminal status early Draining the backlog ahead of the terminal event left the terminal status armed, so whichever chunk emptied the queue stamped the run complete before its terminal event was written — the inverse of the ordering the drain was added to guarantee. Disarm the status for the drain and restore it afterwards. The drain's result was also discarded: a transient Redis failure requeues its batch, and the unconditional reassignment that followed dropped those events even though the budget never rejected them. Keep whatever could not be persisted, and publish the terminal event alone only once nothing earlier is still queued — failing otherwise lets the caller degrade, which records the status without claiming the missing events arrived. Leave eventId unset on a failed resume-path write. Assigning 0 was persisted by clients as a reconnect cursor and rewound them to the start of the run. * fix(execution): keep an event in the buffer when a pressure offload fails Durable compaction runs before an event is queued, so a storage or metadata failure dropped it from replay entirely — a reconnecting client would never see it, even though the live path carried on. Offloading under pressure is only an optimization that keeps a heavy run from exhausting its budget, so when the value cannot be persisted, fall back to buffering it inline: exactly what the run would have done before pressure engaged.
…hain (#6235) Bumps next, @next/env and the @next/swc-* optional deps to 16.3.0 across the root overrides, apps/sim, apps/docs and packages/emcn. Two config notes worth keeping: - `experimental.turbopackFileSystemCacheForBuild`'s default flipped false -> true for stable in 16.3.0, so our explicit `false` is now load-bearing rather than defensive. Without it this bump would have silently re-enabled a build cache measured 3.2x slower on this codebase (#6078). Comment updated to say so. - `experimental.useTypeScriptCli: true` is now pinned. TypeScript 7 ships no JavaScript compiler API until 7.1, so Next's default checker cannot run and needs the project-local `tsc` CLI instead. 16.2.12 was silently skipping build-time type checking entirely because it detected @typescript/native-preview and short-circuited the stage ("Finished TypeScript in 138ms"); pinning the flag keeps that from drifting back. TypeScript toolchain cleanup that the upgrade makes possible: - Drop @typescript/native-preview from apps/sim and apps/docs. It was the pre-release channel for TS 7 and is superseded by typescript@7 (nightlies now ship as typescript@next), and its presence is what suppressed build type checks. - Align packages/browser-protocol and packages/terminal-protocol from typescript ^5.7.3 to ^7.0.2 so every workspace is on one compiler version. apps/sim keeps @typescript/typescript6 as a production dependency: the function sandbox at app/api/function/execute dynamically imports the TS 6 compiler API to transpile user code, and TS 7 has no API to replace it yet. Build and dev were benchmarked 3x per version on a byte-identical tree; the upgrade is performance-neutral (build median 100s -> 99s, dev:full warm 10s -> 9s).
…ath (#6232) * fix(providers): route 6-10MB attachments to the provider large-file path The inline base64 cap was 10 MB of raw bytes, but the execution payload store refuses a single value above 8 MiB and base64 inflates by 4/3. Every raw file over 6 MiB therefore produced a base64 string the store rejected — and the rejection came from the base64 *cache* write, which threw and failed the run with "Execution memory limit exceeded" even though the bytes had already been read successfully. Because shouldUseLargeFilePath only fires above the inline cap, 6-10 MB attachments had no path at all on any provider: they never reached the OpenAI or Gemini Files API upload they were supposed to take. Derive the cap from the payload-store ceiling instead of hardcoding it, and degrade a refused cache write to "not cached" rather than failing a request whose bytes are in hand. Every other size guard in the chain compares raw bytes against maxBytes; only the Redis write sees the encoded size, which is why this went unnoticed — and why it failed only where Redis is configured. Also correct the provider ceilings against the vendors' current documentation: - openai: 50 MiB -> 50,000,000. The gate is `size > maxBytes`, so 50 MiB admitted 52,428,800 bytes; the docs say each file must be *under* 50 MB. - bedrock: had no entry and inherited the inline cap, which is above what Converse accepts (3.75 MB per image, 4.5 MB per document). - groq: 20 MiB -> 20,000,000, and modelled as the request cap the docs actually describe rather than a per-file MiB ceiling. - fireworks: had no entry; its 10 MB budget is on the base64 total, so the raw-byte equivalent is 7.5 MB. Add perRequestMaxBytes for the combined ceilings, enforced before any upload spend, and cover the OpenAI upload path end to end — it had no test at all. * chore(deps): upgrade @google/genai to 2.13.0 and @anthropic-ai/sdk to 0.115.0 @google/genai 2.x reworks the Interactions API, which the Gemini deep-research provider is built on. Migrate it: - `Interaction.outputs` (a flat content array) is now `steps`, a discriminated timeline; the report text lives in the `model_output` steps' text content, alongside thought and tool steps we skip. - `Usage.total_reasoning_tokens` is now `total_thought_tokens`. The old code already fell back to that name through a cast, so this just makes the field the SDK actually returns the typed one. - SSE events renamed: `content.delta` -> `step.delta`, `interaction.start` -> `interaction.created`, `interaction.complete` -> `interaction.completed`. The new event types are discriminated, so the payload casts are gone. Both `interactions.create` calls also stop annotating their params with `Interactions.CreateAgentInteractionParams{,Non}Streaming`. In 2.13.0 those namespace aliases resolve to `CreateAgentInteraction`, whose `stream` is a plain `boolean` rather than a literal — annotating with them erases the discriminant and the call resolves to the union-returning overload, so the result is typed as `Interaction | Stream` at every use. An inline `stream: true as const` keeps the correct overload. Neither upgrade required a `minimum-release-age` waiver: 2.15.0 and 0.115.0 were checked and 2.13.0 is the newest genai release clearing the 7-day window. * chore(deps): upgrade openai to 7.0.0 v5 is the only major with real breaking changes for us; v6 widened a Responses output type and v7 only raised the Node floor to 22, which apps/sim already requires. Three things needed fixing: `ChatCompletionMessageToolCall` became a union of function and custom tool calls, and the custom variant has no `function` field — 43 unguarded `.function` accesses across the OpenAI-compatible providers. Narrow once at each `message.tool_calls` read through a shared `isFunctionToolCall` guard rather than casting at every use. That guard deliberately tests for the `function` payload instead of `type === 'function'`. Many OpenAI-compatible vendors omit `type` on tool calls entirely — our own fixtures do — so discriminating on it type-checks perfectly and then silently drops every tool call those providers return. `ChatCompletionCreateParams.verbosity` narrowed from `string` to a literal union, and the Responses API's output and input item unions now diverge on members Sim never emits (computer-use call outputs, whose `status` admits `failed`, and the `AdditionalTools` escape hatch). Echoing output back as input is what a tool loop is supposed to do, so that conversion is asserted once in convertResponseOutputToInputItems and the streaming loop now routes through it instead of pushing raw output items. The hand-rolled multipart upload in file-attachments.server.ts can now be replaced with the SDK's typed `expires_after` — left for a follow-up so this commit stays a pure upgrade. * fix(providers): correct defects found auditing the attachment and SDK changes The mechanical rewrite that added `isFunctionToolCall` to every `tool_calls` read also rewrote three truthiness guards, where the filtered array was computed, discarded, and the unfiltered value used in the body. Filter once and use that value. The helper also landed between `trackForcedToolUsage`'s TSDoc block and its declaration, leaving that block documenting the wrong function. Raise the Bedrock ceiling from 3.75 MB to 4.5 MB. Converse caps an image at 3.75 MB and a document at 4.5 MB, and a single `maxBytes` cannot express both. Taking the lower bound looked conservative but regressed 3.75-4.5 MB documents, which Converse accepts and which work today. At the document bound every size that works now still works, and only genuinely-too-large files are rejected early; oversized images in that band keep surfacing as a Bedrock API error, exactly as they do without the entry. Both limits re-verified verbatim against the primary docs: Converse's Message reference ("Each image's size ... no more than 3.75 MB", "Each document's size must be no more than 4.5 MB") and Fireworks' vision guide ("Total base64-encoded images must be less than 10MB"). * fix(providers): keep every attachment that works today working Auditing the routing change for backwards compatibility turned up two bands it silently broke. Lowering the single inline cap made the upload path mandatory above ~6 MB, but every large-file path reads its bytes back out of cloud object storage. A deployment without it — local dev, any disk-backed self-host — inlines those files as base64 today and would have started failing outright with "requires cloud file storage". Split the one number in two: the inline ceiling stays at 10 MiB, and a separate threshold marks where an upload becomes *preferable* because the base64 copy no longer fits the payload store. Where no upload path is reachable, base64 hydration now runs to the inline ceiling as before, and a missing cloud-storage backend leaves the file for the inline path instead of throwing. The two strategies also cross over at different sizes now. `files-api` carries every type the provider already accepts, so it takes over at the lower threshold. `remote-url` only fetches images and PDFs, so switching early would have started rejecting 6-10 MB text documents that inline fine today; it takes over only once inlining is genuinely impossible. Revert the Groq ceilings. Its published "20MB" governs a request carrying an image URL, and on this path the body holds only the URL, so it cannot bind on the files maxBytes guards. Groq documents no limit on the image it fetches, so tightening the per-file cap to 20,000,000 and summing raw bytes against the request cap would both reject uploads that work today on no documented basis. * fix(providers): drop the provider ceiling changes and close the audit findings A six-agent line-by-line audit against the vendors' live docs found the `models.ts` ceiling work was not the strict improvement it was written as, so all of it is reverted: - bedrock's 4.5 MB cap broke video. Converse takes image, document AND video blocks, and video is allowed 25 MB base64 — a single `maxBytes` cannot express three content classes, and every 4.5-10 MiB `.mp4` that works today would have started failing. - openai's combined 50 MB cap is the FILE-input limit. Image inputs are governed separately at 512 MB / 1500 images, so summing every attachment rejected eight 8 MB PNGs that OpenAI documents as legal. - fireworks' per-file ceiling was unreachable behind the request budget, while the upload picker went on advertising it — a size the UI accepts and execution always rejects. - The whole `perRequestMaxBytes` feature goes with them: it summed raw bytes against caps that are variously on encoded bytes, on one content class, or on a body that carries only URLs, and it double-counted a file referenced from several messages even though the uploader dedupes by key. Only openai's per-file `maxBytes` stays corrected, to decimal 50,000,000 — the one number a vendor states unambiguously and writes no MiB against. Also fixed, all found by the same audit: The hydration cap stopped short of where `remote-url` actually switches over, so 6-10 MiB attachments on anthropic/openrouter/xai/groq/together/baseten/vllm had neither base64 nor a handle and failed outright — the very band this branch exists to fix. Both decisions now come from one function so they cannot drift. Eight more sites where the mechanical rewrite computed a filtered array and then read the unfiltered one (deepseek, sakana, nvidia, kimi), leaving those providers without the narrowing they appear to have. `isFunctionToolCall` threw on a null or primitive `tool_calls` entry, because `in` requires an object — reachable exactly on the self-hosted gateways this filter was added for. It is now total, and all 32 test mocks match it rather than being quietly more permissive. `checkForForcedToolUsage` in utils/litellm/mistral evaluated the response before the `tool_choice` test, turning a tolerated malformed body into a TypeError on a path that never used to touch it. Gemini: `satisfies` restores the excess-property checking the dropped annotations removed, the poll loop recognises the terminal statuses v2 added instead of spinning for an hour and reporting a timeout, and the streaming doc block no longer names five events that were renamed six lines below it. * fix(providers): report attachment limits in the unit vendors publish The size ceilings are decimal MB — that is how OpenAI, AWS and Fireworks all write them — but the error messages divided by 1024², so OpenAI's 50 MB cap was reported to the user as "48MB". Someone shrinking a 49 MB file to get under it was chasing a limit that does not exist. One formatter, used by all three messages, so the file size and the ceiling in the same sentence are always in the same unit. * fix(providers): derive the limit unit from the ceiling it belongs to The previous commit fixed OpenAI's "48MB" by dividing every ceiling by 10⁶ — which broke the other seven. Only OpenAI's constant is decimal; anthropic, google, together and openrouter are 50 MiB, baseten and vllm 25 MiB, groq and xai 20 MiB. Rendering those as decimal MB overstated each by ~5%, so a 21 MB file on groq was rejected with "(21MB) exceeds the 21MB limit" — a sentence that contradicts itself and sends the user to shrink a file to a size that is still over. Same class of bug as the one being fixed, sign flipped. Both figures now render through one unit taken from the ceiling, so the number a user is told is the number the vendor publishes and the two sizes in a sentence are always comparable. Tested against every ceiling in the registry rather than only the values that happened to round cleanly. Two more from the same audit: A file with a missing or zero declared size was stranded on a files-api provider: hydration bailed on the real byte length while `shouldUseLargeFilePath` saw `0 > threshold` as false, so it got neither base64 nor a handle and failed as "may no longer be accessible" — a size failure wearing an access failure's message. Uploads read the real bytes and enforce the ceiling themselves, so an unknown size now routes to one. The oversized-attachment error blamed the provider for a deployment problem: a files-api provider on a host without cloud storage reported that the provider "has no large-file upload path", which is not true of the provider. `isFunctionToolCall` only proves `function` is present, never that it is well formed, so the trace enricher is defensive again about a hollow payload without giving up the compile-time gate. The 32 test mocks now match production exactly. * fix(providers): stop an over-limit size rendering as the limit itself Deriving the unit from the ceiling fixed the 5% error but left the precision fixed at two decimals, so a file one byte over a 20 MiB cap still printed "(20.00MB) exceeds the 20MB agent attachment limit" — the same self-contradicting sentence, now in a ~5 KB band above every ceiling in the registry. The size rounds up and the ceiling rounds down, so the two can no longer collide. The test that was supposed to guard this asserted a file 0.03MB over and an OpenAI file that was under the limit — neither anywhere near the band — so it passed while the bug was live. It now walks `limit + 1` for every ceiling, and goes red against the old rounding. The reason clause added last commit also claimed a deployment had no cloud file storage whenever the strategy was not inline. A generated document on a remote-url provider reaches that same error with storage fully configured, because a signed URL points at the generation source rather than the rendered artifact — so it was told something false about its own deployment. That case now names itself. * fix(providers): order the attachment failure reason by how general the cause is The generated-document arm was checked first, so it won over both other causes and told users two things that were not true. On an inline-strategy provider — bedrock, mistral, ollama, fireworks, litellm, vertex, kimi — there is no upload path for any file, generated or not, but the message blamed the document format and implied a plain PDF would go through. On openai or google with cloud storage unconfigured it was simply false: a generated document does take the Files API path there, and that exact file uploads fine once storage exists. The one actionable fix was hidden from the operator. A provider with no upload path cannot be helped by changing the file, and a deployment with no object storage cannot reach any upload path whatever the file is, so both now outrank the format-specific case — which is left saying only what is true of it: a signed URL points at the generation source rather than the rendered file. The formatter is unchanged. It was brute-forced over every real ceiling and three million random pairs with no collision or inversion, but the test's six ceilings all divide to exact integers, so floor, round and ceil are indistinguishable on them and the limit-side rounding was unpinned. A ceiling with a fractional remainder now covers it.
…rbosity, and thinking level (#6233) * improvement(agent): allow variable references in reasoning effort and verbosity Reasoning Effort and Verbosity were select-only dropdowns, so a workflow could not sweep them from a variable or an upstream block the way it already can with the model. Both become editable comboboxes, matching the model field directly above them and the managed-agent selectors. - switch both subblocks to `combobox`, keeping their fetched per-model option lists intact - keep them visible when `model` itself holds a reference, since the concrete model id is only known at execution time and cannot be matched against the static capability list - normalize the resolved level in the provider chokepoint so a reference that resolves to `"High"` or to nothing behaves sanely instead of hitting a provider 400 * improvement(agent): allow variable references in thinking level Extends the same treatment to Thinking Level so all three model-tuning fields behave consistently, and logs a level a model does not declare. - switch `thinkingLevel` to `combobox` with the reference-aware condition - normalize it alongside the other two; an empty resolve now takes the deliberate "send nothing" path rather than the incoherent half-state it hit before, and stays distinct from an explicit `none` - warn when a level is not one the model declares, still forwarding it: Sim's per-model lists drive the pickers and can lag a provider, and a sweep needs the provider's own error rather than a silent fallback to the default * improvement(agent): report a model level the sanitizer discards A model bound to a variable or block reference only resolves at execution time, so a run whose reference landed on a model outside Sim's catalogue had its requested level cleared with no signal and quietly fell back to that model's default. Dropping stays the safe default — a provider with no such parameter rejects the whole request — but it is now reported. - log the field, model, and value whenever an unsupported-field level is cleared - cover both diagnostics, including that they stay quiet for a declared level and for the `auto` / `none` sentinels * fix(providers): redact resolved level content from sanitizer diagnostics The model-level fields accept environment and block references, so an unrecognized level is not necessarily a mistyped level — it is whatever the reference resolved to, which can be secret content. The diagnostics added for dropped and undeclared levels echoed it straight into server logs. - log a level only when the catalogue declares it somewhere, or it is an `auto` / `none` sentinel; anything else is reported by length alone - stop discarding levels for a model the catalogue has never seen. Absent is unknown, not known-incapable, and a reference is exactly how a newly released model arrives before Sim catalogues it — the provider decides instead. Models the catalogue knows, and every dynamic-provider id, keep the protective drop * fix(providers): redact the level in Anthropic's unsupported-thinking warning Forwarding an undeclared level is deliberate, but it means the Anthropic adapter receives it and interpolates it straight into its "not supported, ignoring" warning. Since the field is reference-bound, that value can be whatever a mistyped `{{ENV_VAR}}` or block reference resolved to — so the redaction added for the sanitizer's own diagnostics was leaking one layer downstream. - promote the level renderer to `providers/utils` as `describeModelLevel`, the single gate every site echoing a caller-supplied level goes through - use it in Anthropic's warning and in both sanitizer diagnostics * refactor(providers): drop the sanitizer's level diagnostics The two warnings logged server-side, where the workflow author who set the level never sees them, and the surprising case they described — a level discarded for a model newer than the catalogue — is now fixed at the source rather than narrated. They also carried the redaction that leaked resolved content before it was caught, so removing them removes that surface entirely. Levels still normalize, and still drop for a catalogued model that does not take the field. `describeModelLevel` stays for Anthropic's unsupported-thinking warning, which is a pre-existing log this feature newly exposes to resolved reference content.
* fix(docker): upgrade bun to 1.3.14 to unbreak the Next 16.3.0 server
Bun 1.3.13 cannot load Next 16.3.0's compiled server runtime. The app container
runs the Next server under Bun (`oven/bun:1.3.13-slim`, `bun apps/sim/bootstrap.js`),
so every app-page render threw and `/api/health` returned 500:
⨯ Error: Failed to load external module
next/dist/compiled/next-server/app-page-turbo.runtime.prod.js:
TypeError: Expected CommonJS module to have a function wrapper.
If you weren't messing around with Bun's internals, this is a bug in Bun
Isolated to Bun, not Next, by loading that exact module in the real images:
Next 16.2.12 + Bun 1.3.13 -> loads (why staging was fine before)
Next 16.3.0 + Bun 1.3.13 -> CJS wrapper error
Next 16.3.0 + Bun 1.3.14 -> loads
Bun 1.3.14 is the current stable and already fixes it, so this bumps every pin
rather than reverting the framework upgrade, which would only defer the same
latent Bun bug to the next attempt.
Why no gate caught it: local dev machines and this bump's own verification run
Bun 1.3.14, while the container and CI pinned 1.3.13 — and CI only *builds* the
image, it never boots one and probes `/api/health`. A container smoke test in CI
would have caught this before merge; that is worth adding separately.
* fix(docker): align the remaining bun pins with 1.3.14
Two pins were missed in the first pass because the search was scoped to
docker/, package.json and .github/workflows/:
- .devcontainer/Dockerfile still built on oven/bun:1.3.13-alpine
- PI_BUN_VERSION in apps/sim/scripts/pi-sandbox-packages.ts was still 1.3.13,
despite being documented as mirroring the root packageManager field, so Pi
sandbox images would have kept installing the Bun release that cannot load
the Next 16.3.0 server runtime.
Fixed surgically rather than with a repo-wide replace: "1.3.13" also appears
inside SVG path data in apps/sim/components/icons.tsx and
apps/docs/components/icons.tsx, which a blind sed would have corrupted.
|
Too many files changed for review. ( Bypass the limit by tagging |
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 35640005 | Triggered | Generic Password | 5ab5f2c | apps/desktop/src/main/browser-import/import-service.test.ts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryHigh Risk Overview Desktop packaging switches app icons from generated CI and images: Bun 1.3.14 across workflows; a new CodeQL workflow (Blacksmith 8 vCPU, trimmed A small doc tweak in sim-url-state clarifies nullable sort (document chunks example). Reviewed by Cursor Bugbot for commit 856fe0f. Configure here. |
Uh oh!
There was an error while loading. Please reload this page.