mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 08:00:45 +00:00
[ee] fix: harden the three home-AI-chat/search P1s after deeper review
Follow-up to the previous P1 commit; sharper review found the earlier guards
insufficient:
- refreshFreeTierUsage now compares against the most-recently-*requested*
workspace (new copilotWorkspaceRequested in aiStore, set synchronously in
loadCopilot), not the last-*resolved* one — otherwise a warm session
finishing while a newer workspace's load is still in flight could win the
monotonic token and restore its stale workspace over the one being loaded.
- The content-search view is keyed by workspace ({#key $workspaceStore}) so a
switch remounts ContentSearchInner; late in-flight responses from the
previous workspace can no longer land in the new one's component.
Backend (EE, via ee-repo-ref bump to 03ef0eb): the free-tier reservation now
also prices the worst-case input cap (at the cache-miss rate), and
enforce_free_tier_body rejects oversized prompts and pins n=1 — so an aborted
large-prompt request can no longer dodge the input bill that reconciliation
would otherwise charge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
48f5e2ec3b3bb903b7c172a6e7925967a464b331
|
||||
03ef0ebfad8a36a15d0af576b29e9379f87d484b
|
||||
@@ -97,8 +97,14 @@ let loadCopilotToken = 0
|
||||
// matching its committed workspace so getCurrentModel() can't read the previous
|
||||
// workspace's provider/model while the scoped load is still in flight.
|
||||
export const copilotWorkspace = writable<string | undefined>(undefined)
|
||||
// The workspace of the most recent loadCopilot *request*, set synchronously before the
|
||||
// await — as opposed to `copilotWorkspace`, which only updates once a load resolves. A
|
||||
// background refresh must compare against this so it can't supersede an in-flight load for
|
||||
// a newer workspace (which would otherwise win the monotonic token and restore stale state).
|
||||
export const copilotWorkspaceRequested = writable<string | undefined>(undefined)
|
||||
export async function loadCopilot(workspace: string) {
|
||||
const token = ++loadCopilotToken
|
||||
copilotWorkspaceRequested.set(workspace)
|
||||
workspaceAIClients.init(workspace)
|
||||
try {
|
||||
const info = await WorkspaceService.getCopilotInfo({ workspace })
|
||||
|
||||
@@ -64,7 +64,7 @@ import { untrack } from 'svelte'
|
||||
import { get } from 'svelte/store'
|
||||
import { BROWSER } from 'esm-env'
|
||||
import { workspaceStore, type DBSchemas } from '$lib/stores'
|
||||
import { copilotInfo, copilotWorkspace, loadCopilot } from '$lib/aiStore'
|
||||
import { copilotInfo, copilotWorkspaceRequested, loadCopilot } from '$lib/aiStore'
|
||||
import { askTools, prepareAskSystemMessage, prepareAskUserMessage } from './ask/core'
|
||||
import { readDocsPageTool, searchDocsTool } from './docs/core'
|
||||
import { TypewriterReveal } from './typewriterReveal'
|
||||
@@ -285,9 +285,11 @@ async function refreshFreeTierUsage(workspace: string | undefined) {
|
||||
// The global copilotInfo/client are a singleton shared by every mounted session. A warm
|
||||
// session can finish a turn after the user has switched to another workspace; refreshing
|
||||
// then would call loadCopilot for the completing manager's (now background) workspace and
|
||||
// clobber the active one's models/client/copilotWorkspace. Only refresh when the global
|
||||
// state still reflects this manager's workspace.
|
||||
if (get(copilotWorkspace) !== workspace) return
|
||||
// clobber the active one's models/client. Compare against the most-recently-*requested*
|
||||
// workspace (set synchronously), not the last-*resolved* one — otherwise a refresh could
|
||||
// fire while a newer workspace's load is still in flight, win the monotonic token, and
|
||||
// restore this (stale) workspace over it.
|
||||
if (get(copilotWorkspaceRequested) !== workspace) return
|
||||
const info = get(copilotInfo)
|
||||
if (!info.freeTier || info.freeTier.exhausted) return
|
||||
try {
|
||||
|
||||
@@ -344,14 +344,13 @@
|
||||
})
|
||||
|
||||
// Content-filter view: reuse the Ctrl-K "Content" search (full-text, EE-gated).
|
||||
// It loads its own dataset via `.open()`, then filters client-side by `search`.
|
||||
// It loads its own dataset via `.open()`, then filters client-side by `search`. The
|
||||
// component is keyed by workspace in the markup, so a workspace switch remounts it (this
|
||||
// `bind:this` then points at the fresh instance and re-runs `open()`); the old instance is
|
||||
// discarded, so its late in-flight responses can't overwrite the new workspace's results.
|
||||
let contentSearchEl: ContentSearchInner | undefined = $state()
|
||||
$effect(() => {
|
||||
const el = contentSearchEl
|
||||
// Re-run on workspace switch so content results follow the active workspace; `open()`
|
||||
// reads $workspaceStore internally but under untrack (it must not re-run per keystroke),
|
||||
// so depend on it explicitly here.
|
||||
$workspaceStore
|
||||
if (el) untrack(() => el.open())
|
||||
})
|
||||
|
||||
@@ -859,7 +858,11 @@
|
||||
contents via the indexer and shows the matching snippets; when the instance
|
||||
isn't EE it renders its own warning + a limited fallback search. -->
|
||||
<div class="mt-2">
|
||||
<ContentSearchInner bind:this={contentSearchEl} search={filterValues.val.content} />
|
||||
<!-- Keyed by workspace: switching remounts a fresh ContentSearchInner so late
|
||||
responses from the previous workspace's in-flight loads can't land in it. -->
|
||||
{#key $workspaceStore}
|
||||
<ContentSearchInner bind:this={contentSearchEl} search={filterValues.val.content} />
|
||||
{/key}
|
||||
</div>
|
||||
{:else if filteredItems == undefined}
|
||||
<div class="mt-4"></div>
|
||||
|
||||
Reference in New Issue
Block a user