Remove the sqlx cache file for the test query deleted when the gate-alert
fixture was rewritten, and revert cargo fmt reformatting of three
unrelated test files.
Addresses the review on the oversubscribed-gate alert:
- A new instance setting, critical_alerts_on_oversubscribed_gates (Alerts
group), gates the alert at the monitor call site so it applies without a
restart. Defaults on. The boolean Toggle now honors defaultValue so the
UI renders enabled-when-unset rather than showing off while the backend
sends alerts.
- Arrivals are measured from concurrency_first_gated_at (new nullable
column) rather than created_at, so jobs pushed with a future
scheduled_for that only reach the gate once due are not mistaken for no
arrivals. Emitter change is in windmill-ee-private (ee-repo-ref bump).
Verified the toggle renders enabled-by-default in the browser.
This commit updates the EE repository reference after PR #674 was merged in windmill-ee-private.
Previous ee-repo-ref: 23ab03d1c0c14d40fa7208901df1138136d7e200
New ee-repo-ref: e5c26f6401d3761b9c27a2361844de6dd35ee593
Automated by sync-ee-ref workflow.
A new alert with hardcoded thresholds should have an off switch. Adds
critical_alerts_on_oversubscribed_gates to the Alerts group in instance
settings, gated at the monitor call site so the toggle applies without a
restart.
Defaults to on: an oversubscribed gate is otherwise invisible, since the
queue-depth alert deliberately excludes the jobs it parks.
Emitter change (the disable hint in the alert body) lives in
windmill-ee-private (see ee-repo-ref.txt bump).
The growth check asked whether a day's worth of jobs had arrived, so the
alert only tripped once roughly a day's backlog had accumulated behind
the gate. Compare against what the gate admits over an hour instead, and
drop the drain-horizon condition, which the growth check already implied.
Emitter changes live in windmill-ee-private (see ee-repo-ref.txt bump);
this commit carries the regression test and the query cache.
This commit updates the EE repository reference after PR #673 was merged in windmill-ee-private.
Previous ee-repo-ref: 8df33392d864b87731ab5742247b1aa3f6dacf80
New ee-repo-ref: a31d6ae5e573d549e00e73f08b694a64a60ca119
Automated by sync-ee-ref workflow.
A single freshly cancelled job, which no gate holds, vouched for every
stale mark on that gate, and recovery landed globally instead of with the
workspace the alert was scoped to.
Also fixes a test helper that cancelled every uncancelled job on the tag
rather than the rows it inserted, which made the new liveness regression
pass for the wrong reason.
Emitter changes live in windmill-ee-private (see ee-repo-ref.txt bump).
A non-null gate mark is not evidence a gate still holds the job, and one
recent arrival is not growth: a queue draining at its ceiling always
leaves recent survivors, so a shrinking backlog paged. Gates that drain
fully also left their alert open forever.
Emitter changes live in windmill-ee-private (see ee-repo-ref.txt bump);
this commit carries the three regression tests and the query cache.
Excluding gate-parked jobs from the queue-depth alert left a real
incident unreported: when jobs arrive faster than a gate admits them the
backlog only grows and those jobs never run, silently.
Adds a distinct per-gate alert for that, wired into the monitor beside
the queue-depth one. Emitter lives in windmill-ee-private (see
ee-repo-ref.txt bump); this commit carries the CE stub, the monitor
wiring, the tests and the query cache.
The offline cache was regenerated without test targets, so the
sqlx::query! calls in jobs_waiting_alerts had no entries and any build
under SQLX_OFFLINE failed on them.
This commit updates the EE repository reference after PR #671 was merged in windmill-ee-private.
Previous ee-repo-ref: e1e73f4da9349b2aa58d4e113ffae2cb3dc6646c
New ee-repo-ref: 1ef75b5f789c7f9bae66c4be09cf82727426241d
Automated by sync-ee-ref workflow.
The index on v2_job_queue ran as an ordinary CREATE INDEX inside the
migration transaction, holding the locks taken by the preceding ALTERs
while scanning a live, actively written queue. Register the migration in
OVERRIDDEN_MIGRATIONS so it is rewritten to CREATE INDEX CONCURRENTLY and
executed as top-level statements, matching the other queue indexes.
Soft cancellation also leaves a stale gate mark on a job the pull path
will hand straight to a worker, so a cancelled job now counts as ungated
whatever mark it carries.
Emitter and limiter changes live in windmill-ee-private (see
ee-repo-ref.txt bump).
Gate ids are global, so the liveness aggregate covers the whole queue and
running it per monitored tag let an unrelated backlog scale the cost of
every tag. It is now resolved once per alert config, skipped when marks
cannot be trusted, and backed by a partial index over parked jobs.
Emitter and limiter changes live in windmill-ee-private (see
ee-repo-ref.txt bump).
Workers may lag the server, so the exclusion is gated on
MIN_VERSION_SUPPORTS_CONCURRENCY_GATE_MARK: until every worker stamps,
the alert counts the whole backlog as it did before marks existed.
Concurrency keys are global and one key can carry several limit/window
policies, so the stamped column becomes concurrency_gate_id, holding the
full admission policy, and gates are looked up across tags.
Emitter and limiter changes live in windmill-ee-private (see
ee-repo-ref.txt bump).
Freshness was taken as the newest mark anywhere on the tag, but gates
free independently, so a key still being refreshed vouched for stale
marks left by a different key that had since freed.
Adds v2_job_queue.concurrency_gated_key, which the limiter stamps
alongside the timestamp, so liveness is derived per gate.
Emitter and limiter changes live in windmill-ee-private (see
ee-repo-ref.txt bump).
A mark records that a gate held the job when the limiter last looked at
it, and only a worker re-attempting the job refreshes it. Nothing clears
it when the gate later frees, so if workers then disappear the backlog is
genuinely starved while still carrying marks.
Store concurrency_gated_at rather than a boolean, so the alert can tell a
mark the limiter is still refreshing from one that stopped advancing.
Emitter and limiter changes live in windmill-ee-private (see
ee-repo-ref.txt bump).
The alert reconstructed the limiter's admission test in SQL to tell a
gate-parked backlog from worker starvation. That test spans the running
counter, completions inside the time window, per-version setting
fallbacks and three bypass paths, and nothing linked the two copies, so
each divergence surfaced as a fresh false alert or a suppressed one.
Adds v2_job_queue.concurrency_gated, which the limiter sets when it
re-queues a job it could not admit; the alert counts unmarked jobs. The
query drops to a single scan with no joins.
Emitter and limiter changes live in windmill-ee-private (see
ee-repo-ref.txt bump).
The pull path skips concurrency limiting whenever canceled_by is set, so
a cancelled job waits for a worker rather than for a gate, and reading it
off key saturation let a saturated key hide a cancelled backlog during a
worker outage.
Emitter change lives in windmill-ee-private (see ee-repo-ref.txt bump).
Adds the cancellation regression test and refreshes the query cache.
The classifier derived saturation purely from stored counter and window
state, but the limiter admits unconditionally when
DISABLE_CONCURRENCY_LIMIT is set or the resolved key is empty, so
excluding on a gate in those modes suppressed the alert during a genuine
capacity outage.
Emitter change lives in windmill-ee-private (see ee-repo-ref.txt bump).
Adds the empty-key regression test and refreshes the query cache.
The saturation check read only concurrency_counter.job_uuids, but the
limiter admits on running jobs plus those that ended inside
concurrency_time_window_s, so a gate held shut purely by recent
completions read as free and its backlog raised the same false capacity
alert one completion later.
Emitter change lives in windmill-ee-private (see ee-repo-ref.txt bump).
Adds the regression test for that case and refreshes the query cache.
Keep the misfire repro and a single guard covering both the plain and
the unsaturated-gated branches of the classification. The dropped test
asserted alert-body wording and re-exercised counting behavior the fix
does not change.
The "Jobs waiting in queue" critical alert counted every non-running job
in v2_job_queue past the time threshold, conflating fleet capacity
shortage with a workspace's own concurrency limit holding its jobs back.
Only the former is actionable by whoever is paged, so a single runnable
with concurrent_limit=1 and a large backlog pages repeatedly against a
near-idle fleet.
Emitter change lives in windmill-ee-private (see ee-repo-ref.txt bump).
This commit carries the regression tests and the offline query cache.
* feat(ai): live web-search source list on chat tool cards
* fix(ai): restrict web-search source links to http(s), respect user collapse
* fix(ai): read plural queries field for openai web-search labels
* feat(sessions): live DOM access for the raw-app preview in AI sessions
Give the session chat read-only access to the rendered raw-app preview,
plus inspector-picked element chips synced bidirectionally with the preview.
- search_dom / read_dom tools: live, same-origin contentDocument reads by CSS
selector (selector omitted = whole body), reusing the file engine over
pretty-printed outerHTML (worker-guarded, bounded). Session-gated.
- Inspector picks become app_dom_selector context chips (selector-only); the
model fetches content on demand. Chips shown even in GLOBAL/session mode.
- Multi-select chips synced with the preview: the chip list is the source of
truth, pushed to the harness which renders one highlight per selector; add,
chip-remove, and preview-× remove all stay in sync. Overlays stripped from
search_dom output.
The ui_builder harness changes (multi-select highlights + unique nth-of-type
selectors) live in windmill-code-ui-builder and ship via the artifact re-pin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): inline element prompt + inspector polish for raw-app DOM
Session preview:
- Add InlineElementPrompt: a floating mini-composer anchored top-left over a
selected element; sends a chat turn scoped to that element (its chip is
context). Autofocus + remount per selection, borderless input, label gutter
so it clears the harness name+size pill.
- autosize action gains an optional minHeight (default 30) so the compact
inline input renders a tight single line.
- Fix exiting select mode: Esc / inspector toggle-off now fully clear the
session's DOM-selector chips (source of truth) so the overlays AND the inline
prompt are dismissed together, instead of leaving them stranded.
Full-page raw-app editor:
- Add inspectorHoverOnly: the inspector highlights on hover (outline +
name/size) but a click selects nothing — no persistent selection, no app-mode
context pick, no inline prompt.
Context badges:
- ContextElementBadge gains a compact prop (passed only by AIChatMessage): the
DOM-selector chip and tagged workspace items (app/script/flow) render smaller
above a sent message, while every badge stays the same size in the composer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): address Codex review P1s on raw-app DOM context
- Inline element prompt: queue the turn when the chat manager is already
streaming (mirror the composer) instead of a concurrent sendRequest that
would race the shared abortController / streaming buffers.
- DOM selector chips now carry the raw-app path they were picked from. Each
preview tab renders/pushes only its own app's chips, and a tab clears
cross-app chips when it becomes the active DOM target — a selector could
otherwise silently resolve against whichever raw-app tab is active
(search_dom/read_dom target the active preview).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(raw-apps): re-pin ui_builder artifact to f79e1c3
Bumps the UI Builder artifact to the release built from ui_builder main
after windmill-code-ui-builder#18 merged (the multi-select DOM inspector
synced with the host chat). This is the artifact re-pin step that gates
merging this PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): inline-prompt vertical centering + auto-collapse DOM tools
- InlineElementPrompt: bias the textarea padding (pt-[5px] pb-[3px], same
total) so the single line sits centered in the pill — the shared autosize
floor otherwise adds slack at the bottom and the text read 1px high.
- search_dom / read_dom: drop the explicit `autoCollapseDetails: false` so the
tool card collapses after a successful read like other tools (it was copied
from get_app_runtime_logs, where staying open is intentional).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): address Codex review on raw-app DOM access
- [P1] Release the DOM-requester slot when the active raw-app tab is hidden.
Preview tabs stay mounted, so switching to a flow/script/page (or another
raw-app) tab left search_dom/read_dom targeting the now-hidden raw app. The
slot is now claimed with a per-tab owner token and released via the effect
cleanup; the runtime only lets the claiming tab clear it, so a set/release
race between two raw-app tabs can't blank the new owner.
- [P2] Strip the inspector outline classes from the clone ROOT (the selected
element), not just its descendants — querySelectorAll skips the root, so a
scoped read of the selected element leaked inspector-picked in its outerHTML.
- [P2] Reword the AIChatDisplay context-chip comment to drop drafting-history
narration ("now", "not as rows here") per AGENTS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): scope a queued inline prompt to its own element (Codex P1)
An inline element prompt submitted while a turn was streaming got queued as
plain text; if the user selected another element before the queue flushed, the
queued turn snapshotted the new selection and sent scoped to the wrong element
(or none).
The queue now carries a context snapshot:
- queueMessage(text, context?) pins the selection present at submit time.
- sendRequest accepts contextOverride and uses it verbatim as the turn's
selected context (feeds both the optimistic bubble and the API message).
- All three queue-flush sites thread the snapshot through and restore it if the
auto-send bails.
- With an override, only the queued message's own DOM chips are consumed from
the live selection — a newer selection made since is left intact — preserving
the one-shot chip semantics.
onInlinePrompt snapshots getSelectedContext() when it queues.
(Codex's other P1 — full-page-editor hover-only no longer attaching elements to
App AI — is intentional per an explicit product decision, left unchanged.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(raw-apps): restore full-page editor App-AI inspector context
Codex/Pi flagged a real regression: passing inspectorHoverOnly disabled the
full-page editor's existing App-AI (app-mode) inspector context. Clicking an
inspected element no longer set inspectorElement, so the app-mode
SelectedContext badge and the element attach in prepareAppUserMessage were
dead.
inspectorHoverOnly and the session path (onInspectorSelect) are mutually
exclusive — the flag was only ever set in the full-page/app-mode context, so it
only suppressed the app-mode path. Remove it entirely (prop, guard, and the
pass in +page.svelte), restoring the original click -> inspectorElement ->
app-mode context behavior. Session behavior is unchanged; the hover highlight
still works in both.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): snapshot inline-prompt context synchronously (Codex P1)
The immediate inline-prompt path called sendRequest without a context override,
so the selected element was read only after the async send preflight
(attached-file refresh, beforeSend, global-skill refresh). Picking another
element during that window attached the wrong one to the outgoing turn.
onInlinePrompt now snapshots the selection synchronously at submit time and
passes it as contextOverride on both the queued and immediate paths, so the
prompt always rides with the element it was scoped to.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): drop a queued inline prompt's DOM chips when the app changes
A queued inline prompt scoped to raw app A described A's elements, but the
runtime's single DOM requester targets whichever preview is active. Switching
to app B before the queue flushed left the turn describing A while search_dom /
read_dom would query B.
When a raw-app preview tab becomes active it now also strips the queued turn's
DOM chips belonging to other apps (dropQueuedDomContextForOtherApps), so a
queued prompt can't ask the model to read one app's selectors against another's
live DOM. Non-DOM queued context is preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): scope inline prompt to its anchored element (Codex P1)
The inline prompt sits over a single element but snapshotted EVERY selected DOM
chip. With several elements selected (Shift-multi-select), a prompt shown over
B sent both A and B as context, so "change this button" couldn't identify which
one it meant.
onInlinePrompt now uses its anchored `selector` argument (previously ignored)
to keep only that element's DOM chip in the snapshot; non-DOM context is
preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): preserve original context on edit/retry (Codex P1)
restartGeneration resent with the live selection, so editing or retrying an
element-scoped prompt lost its DOM selector (or adopted a newer one) — DOM
chips are one-shot and cleared from the live selection after the first send.
It now passes userMessage.contextElements as contextOverride, re-using the
exact context the message was originally sent with. undefined (modes that don't
attach contextElements) falls back to the live selection as before.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): route search_dom/read_dom to the selector's own app preview
The DOM requester was a single active-tab slot, so search_dom/read_dom always
read whichever preview was visible. A turn scoped to app A that read the DOM
after the user switched to app B would silently read B (Codex P1).
Preview tabs stay MOUNTED when hidden, so app A's DOM is still there — the fix
is to route by app instead of "active tab":
- Every mounted RawAppEditorView registers its DOM requester keyed by its app
path (runtime holds a map, not one slot); the visible tab is tracked
separately as the default target.
- search_dom / read_dom gain an `app_path` (surfaced per chip in the SELECTED
DOM ELEMENTS block and the tool schema); the runtime routes the query to that
app's still-mounted preview. No app_path → the active preview, or the only one
open.
- If the named app's preview has been closed, the tool returns "The preview for
X is no longer open…" so the chat can explain it, rather than reading the
wrong app.
This makes the queued-context drop-on-switch (dropQueuedDomContextForOtherApps)
redundant — removed. The lightweight submit-time snapshot (which element the
message is about) is kept.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): address Codex review on DOM access (script strip + app-scoped chips)
- search_dom/read_dom: strip the app's compiled <script> bundle from the
whole-body clone so queries only see rendered HTML, not source (P1).
- DOM chip add-dedup and removal now match both selector AND app path, so an
identical selector in another app can't drop or block a chip (P2).
- Refresh stale single-slot requester comments to describe per-app routing;
reword the foreign-chip reset to reflect why it stays (composer chips are
unlabeled by app) (P2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): scope DOM-chip clear to its app so a rebuild can't wipe another (Codex P2)
Every mounted preview emits inspectorClear on rebuild, routed to
onInspectorClearAll → clearSelectedDomElements(). Unscoped, a hidden app A
rebuilding cleared app B's active chip and highlight. clearSelectedDomElements
now takes an optional appPath; the preview-clear path passes its own path, while
post-send and foreign-reset clears stay unscoped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): reject a <script>-root DOM query so the app bundle can't leak (Codex P2)
The descendant strip (querySelectorAll('script')) skips the clone root, so a
search_dom / read_dom query whose selector targets `script` serialized the whole
compiled bundle. Reject a script root with an explanatory result before cloning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): union queued inline-prompt DOM context + reject inspector-label root (Codex)
- Multiple inline prompts queued during one stream accumulate their text, but
queuedContext replaced (last wins) — dropping an earlier element's chip and
misapplying its instruction. Union the DOM selector chips across queued prompts
(non-DOM context still from the latest snapshot) (P1).
- search_dom/read_dom: reject an .inspector-label clone root, like the <script>
root — the descendant strip skips the root, so a `.inspector-label` query
would return inspector chrome (P2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): restore consumed DOM chips when a turn is cancelled unsent (Codex P1)
DOM selector chips are consumed from the live selection before the request goes
out. If the user hits Stop before any usable output, restoreUnsentTurn put the
text/pastes/images back but not the chips, so resending the restored prompt lost
its element scope. Re-add the consumed chips on rollback (skipped on a queued-
message handoff, which carries its own context).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): edit box edits the message's own context, sends what it shows (Codex P1)
Editing a past message bound the edit box to the LIVE selection while the resend
carried the message's original contextElements — so the box showed one set of
chips (or none) but sent another, and add/remove in the box did nothing. The edit
box now edits a copy seeded from the message's own contextElements, and
restartGeneration sends that edited copy; a bare retry still falls back to the
original. Drops the now-unused selectedContext prop from AIChatMessage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): keep DOM context coherent on dequeue/cancel + per-chip edit identity (Codex)
- dequeueMessage dropped the queued draft's pinned DOM context, so returning it
to the composer sent the live selection instead. Restore the draft's context on
dequeue (P1).
- Cancel-rollback re-added this turn's chips additively, mixing in chips selected
mid-stream. Replace instead so the restored draft stays coherent — shared helper
#restoreDomContext used by both paths (P1).
- The context chip row keyed/removed by (type, title); repeated DOM elements share
a title (two button.btn), so editing a multi-select message gave duplicate keys
and deleting one chip removed both. Identify DOM chips by (appPath, selector) (P1).
The queued-prompt instruction↔element association (Codex #1) is the accepted union
tradeoff and is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): scope read_dom pagination + don't consume live chips on edit/retry
Codex P1s:
- read_dom's continuation note only renamed read_file, so following it with
start_line alone dropped app_path/selector and re-read the active app's whole
body instead of continuing inside the element.
- contextOverride also carries an edit/retry's replayed copy of an older
message's context. Consuming it from the live ContextManager stripped an
identical chip the user had since selected in the composer, leaving their
draft unscoped. Overrides now declare their origin ('pinned' vs 'replay').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): keep DOM context paired with the draft it belongs to
Restoring a draft's pinned DOM chips replaced the live selection unconditionally,
but both restore paths can leave a different draft in the composer:
- Cancel: an occupied composer declines the rolled-back text (restoreInstructions
bails), yet the cancelled turn's chips still replaced the live ones — the draft
the user typed during the stream kept its text but got retargeted.
- Dequeue: queued text is prepended onto an existing draft, so both instructions
share one composer; replacing the chips dropped the standing draft's element.
restoreInstructions/prependText now report whether the composer took the text.
Cancel restores context only when it did; dequeue keeps both drafts' chips when
it merged into one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): scope an inline prompt's chip filter to its own app
The filter narrowing a multi-select down to the anchored element matched on
selector alone. Selectors are generated per app and collide across them, so an
inline prompt in app B also carried app A's identically-named chip, leaving the
model two indistinguishable referents. Since draft restoration can now
legitimately hold chips from several apps, match (selector, appPath) — as the
sibling deselect/clear handlers in this file already do.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FlowBuilder's deploy branches solely on the `newFlow` prop. When an embedder
mounts the builder with `newFlow` false for a flow that was never deployed,
Deploy issues `PUT /flows/update/{initialPath}` and the backend answers
`Flow not found at name <path>`, so the first deploy can never land.
Confirm against `GET /flows/exists/{path}` before taking the update branch,
and create instead when nothing is deployed there. The create path and the
full-page editor (where `newFlow` is derived from `no_deployed`) are
unchanged.
Fixes WIN-2206
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: cap queued jobs per concurrency key on cloud
* fix: close preprocessed-flow bypass and bound concurrency cap scan
* fix: only cap concurrency keys with an active concurrent_limit
* chore: only load concurrency key cap setting when cloud hosted
* fix: reject queued-job import on cloud
* fix(apps): default a missing grid so the app preview does not crash
* fix(apps): migrate the adopted draft so the editor matches the viewer
* docs: tighten grid-normalization comments to durable constraints
* test: drop the migrateApp grid-default test
* feat: expose windmill api endpoint catalog to global ai chat
* fix: guard variable reads and deletes in ai chat api catalog
* fix: clarify api catalog prompt example for run result access
* fix: block deleteScriptByHash and document resource read boundary
* fix(security): enforce token scope filtering on folder list endpoints
* fix: apply folder scope filtering before pagination
* fix: keep SQL pagination for unrestricted tokens on folder lists
* fix: bound scoped folder-list scan with chunked pagination
* fix: use keyset pagination for scoped folder-list scan
* refactor: reduce folder scope fix to the minimal predicate filter
* chore: drop folder list scope test
`list_schedule` and `list_schedule_with_jobs` returned every schedule in
the workspace regardless of the token's declared scope. A token limited
to `schedules:read:<prefix>/*` could enumerate all schedule paths (and
their script paths, cron expressions and recent job outcomes) through
these two endpoints, while `get_schedule` correctly rejected them.
Apply `build_scope_path_predicate(&authed, "schedules", "read")` to the
returned rows, mirroring scripts, flows, apps, resources and variables.
In `list_schedule` the filter runs after the draft-only append so both
deployed and draft rows are covered.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`get_args_from_history_or_saved_input` looked up saved-input rows by
`id + workspace_id` only, without checking ownership or `is_public`. The
`input` table has no RLS policy, so any authenticated workspace member
could read another member's private saved-input args (which may hold
credentials) just by knowing the UUID, even though `inputs/list` already
scopes to `is_public OR created_by = caller`.
Add `AND (is_public IS true OR created_by = $4)` to both `input`-table
queries in the handler (the `?input=true` single-query branch and the
default `v2_job UNION ALL input` branch), matching the predicate already
used by `list_saved_inputs`.
The `v2_job` branches are already covered by the table's RLS policies
enforced via `SET LOCAL ROLE windmill_user` in `UserDB::begin()`, so they
are left unchanged.
Fixes WIN-2202
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With syncBehavior v1 the pull strips the user-specific on_behalf_of_email /
permissioned_as from metadata and keeps a boolean marker so a later push can
preserve remote ownership. The marker was written unconditionally as
`!!<field>`, so every ownerless script, flow, schedule and trigger (the vast
majority) got a `has_on_behalf_of: false` / `has_permissioned_as: false` line,
producing a spurious diff on every pull.
Absence of the marker already means "no owner" everywhere it's read on push, so
only emit the key when true.
Fixes WIN-2201
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the ScriptBuilder whitelabel pattern in FlowBuilder so the
`customUi.topBar.path` and `customUi.topBar.editablePath` options actually
take effect:
- add `editablePath?: boolean` to `FlowBuilderWhitelabelCustomUi.topBar`
- gate the EditorHeader block on `customUi?.topBar?.path != false` and pass
`pathEditable`/`summaryEditable` (the latter wires the pre-existing but
unused `editableSummary` flag)
- hide the Path field in FlowSettings when `topBar.editablePath` is false
Defaults are unchanged (`!= false`), so non-whitelabel flows still show the
path header and settings field.
Fixes WIN-2200
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to #10179. The exponential back-off between reconciler re-arm
retries of a persistently-failing schedule capped at 32 passes (~2.7h at
the default 5-min reconcile cadence). Lower the cap to 8 (~40min) so a
schedule fixed out of band (a lapsed license renewed, a bad cron corrected
directly in the DB) auto-recovers within a few passes, while still cutting
the retry rate sharply versus retrying every pass. Fixes via the UI/API
re-arm immediately and are unaffected.
Fixes WIN-2198
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The scripts list endpoint (include_draft_only=true) only populated
auto_kind for the pipeline case, leaving library draft-only scripts
(no `main` function) with auto_kind: null even though the frontend
saves auto_kind: "lib" into the draft JSON. This made it impossible
to distinguish library draft-only scripts from regular ones without a
separate per-script API call.
Fall back to the auto_kind saved in the draft value after the
content-derived pipeline check, which keeps priority since it mirrors
the deploy-time computation.
Fixes WIN-2199
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(schedules): stop disabling schedules on transient push errors
A scheduled flow whose next-occurrence push failed after retry exhaustion
used to be disabled, killing a healthy schedule over a transient DB blip
(pool contention, statement timeout). Now that the unarmed-schedule
reconciler exists (#10174), transient failures no longer disable: the
current occurrence runs to completion and the reconciler re-arms the next
occurrence once this run leaves the queue.
In the flow schedule-push path after retry exhaustion we now branch on the
error: QuotaExceeded/NotFound still disable (the schedule's own fault, and
rearm_schedule would otherwise leave them enabled-yet-unarmed forever),
while transient errors are only reported and the flow continues.
The previous iteration returned a SchedulePushZombieError to force a zombie
restart; that is removed, because zombie detection cancels (does not
restart) same-worker flows, so it would have lost the current run of a
same-worker scheduled flow. The now-obsolete SchedulePushZombieError type
and its catch in worker.rs are deleted.
Fixes WIN-2198
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(schedules): back off and surface repeated reconciler re-arm failures
The unarmed-schedule reconciler retried a schedule that could not be
re-armed on every pass, forever, logging only to the server. With the flow
schedule-push path no longer disabling on non-transient errors, a
persistently-broken push (bad stored cron/timezone/args, lapsed license
key) now stays enabled and would spin in that loop silently.
The reconciler now tracks consecutive re-arm failures per schedule:
exponential back-off (2, 4, 8, … passes, capped) between retries so a
broken schedule is not hammered, and after 3 consecutive failures it
surfaces the cause once (records schedule.error + raises a critical alert)
without disabling. Both reset the moment the schedule re-arms, which also
clears the recorded error.
Verified end-to-end on a running server: a flow schedule with a corrupted
cron stays enabled, retries back off, the error is surfaced after the
third failure, and it re-arms and clears the error once the cron is fixed.
Fixes WIN-2198
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
is_persistent_control_path_dir_name is only referenced from
prepare_socket_root, which is #[cfg(unix)]. On Windows nothing used it,
so -D dead-code failed the build. Gate the function and its unit test to
unix, matching the reaping code they support.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#10176 removed `path_field_renames` from `EndpointTool`, while #10162 added a
new `ep()` test constructor in runner.rs that still set it. Each passed CI
against its own base, but together on main the test build fails with E0560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(frontend): scope session pipeline trigger editors to the session workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): hoist triggerWorkspace decl above GCP init-time getBaseUrl call
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): scope nested trigger pickers to the session workspace
The triggerWorkspace resolver scoped direct trigger CRUD calls to the
session's (forked) workspace, but nested pickers still defaulted to the
nav `$workspaceStore`: in a fork session, resource lists/creation,
variable creation, and path-existence checks ran against the parent
workspace while save/delete targeted the fork — misleading options and
false path-validation failures.
Thread `wsId` into the nested controls of the 9 pipeline-canvas kinds:
- `<Path workspaceOverride={wsId}>` (8 editors) — path + folder checks
- `<ResourcePicker workspace={wsId}>` (6 config sections; add the
resolver to MqttEditorConfigSection, which lacked `wsId`)
- SQS `<VariableEditor workspace={wsId}>` — variable creation
Also drop the per-site `wsId` rationale comment repeated across ~20
files; the invariant is documented once in triggerWorkspace.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): scope trigger runnable picker to the session workspace
The runnable picker (`ScriptPicker`, reached via `TriggerRunnablePicker`
and directly in the schedule editor) listed scripts/flows/apps from the
nav `$workspaceStore` with no override, so a forked session offered the
parent workspace's runnables when attaching a script/flow to a trigger.
Add an optional `workspace` prop to `ScriptPicker` (defaults to
`$workspaceStore` → no change for existing callers), pass it through
`TriggerRunnablePicker`, and wire `wsId` from the 7 trigger editors that
use it plus the schedule editor's 3 direct pickers.
Completes the nested-picker workspace scoping; the excluded kinds
(azure/http/websocket/native) keep their own ScriptPickers unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): scope trigger error-handler, folder default, and runnable actions
Follow-up to the trigger-workspace scoping: three subtrees still read the
nav `$workspaceStore` in a forked session.
- ErrorOrRecoveryHandler (via TriggerRetriesAndErrorHandler in the 7 in-scope
editors): add a `workspace` prop (defaults to `$workspaceStore`) and route
handler lookup/schema, Slack/Teams settings, test jobs, and run links
through it, so the error handler is resolved/tested/saved in the session
workspace instead of A while the trigger lives in B.
- useFolderDefaultPermissionedAs: accept an optional workspace getter so a
`f/...` trigger's default permissioned-as is read from the session
workspace, not the nav one (PermissionedAsLine passes `() => wsId`).
- ScriptPicker actions: scope the View drawer (`getScriptByPath`) and
`FlowPathViewer` to `effectiveWorkspace`, and carry `?workspace=` onto the
Edit/View routes when an explicit override is set (the layout consumes the
param, same mechanism as editInFork). The param is only appended when a
workspace override is passed, so existing callers' links are unchanged.
Also consolidate the repeated workspace-scoping comment in
PipelineTriggerEditors (the invariant lives in triggerWorkspace.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): scope error-handler/schedule nested consumers to the session workspace
Address CI Codex review on #10032 — the error-handler and schedule
subtrees still had nested consumers reading the nav workspace:
- ErrorOrRecoveryHandler: pass the resolved workspace to its own nested
`ScriptPicker` (custom-handler list + View/Edit) and add a `workspace`
prop to `ChannelSelector` (Teams channel listing); carry the acting
workspace onto the "create from template" link.
- ScheduleEditorInner: pass `workspace={wsId}` to the error/recovery/
success `ErrorOrRecoveryHandler` panels, `workspaceId={wsId}` to
`WorkerTagPicker`, and the workspace query param onto the dynamic-skip
template link.
Template-link and picker overrides only diverge from `$workspaceStore`
when a session override is set, so non-session callers are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): add image attachments and agent raw-app screenshots
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(ai-chat): generalise take_screenshot fidelity caveat
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): keep compaction boundary on a displayed user message
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(raw-apps): count line boxes by vertical overlap, not rect count
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): enforce vision gating and bound image attachments
Refuse images on known text-only models instead of warning and sending them
anyway; cap input bytes before decode; keep clipboard text when it accompanies
a bitmap; don't queue a message whose images can't ride the plain-text queue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* perf(ai-chat): trim take_screenshot schema and shrink the card's copy
Move the fidelity caveat from the tool def onto the tool result: the def is
re-sent every global iteration (~258 tok), while the caveat only matters once a
capture exists. Keep a downscaled copy in displayMessages when it is actually
smaller — those are never compacted and are re-cloned on every saveChat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): carry attached images through the message queue
Enter during a streaming turn queued the text and silently dropped the images,
so the auto-send was not the message the user submitted. The queue now holds
both, moved together via takeQueue/clearQueue/restoreQueue so none of the three
flush sites, the dequeue-to-composer path, or the two conversation-switch drops
can leak one without the other.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): gate screenshots on vision, narrow when the tool fires
take_screenshot buffered an image unconditionally, so a text-only model got an
image_url and rejected the turn; the attach-time check never covered it, nor a
model switched after attaching. Gate before capture and again at send.
Only reach for the tool when the user raises how the app looks, rather than
after every UI edit.
A collapsed preview keeps the iframe mounted at zero width, passing the ready
checks and then failing inside the rasteriser as '[object Event]'. Name it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): hold sending while attachments decode
addImages read the free-slot count before its await and appended after it, so a
send during the ~50-800ms decode cleared images while the closure still wrote to
them, landing the picture on the following message; two drops also claimed the
same slots and could pass the cap. Reserve slots up front, block sending until
they resolve, and show a placeholder so the held send is explained.
Keep only a bounded copy in the transcript: displayMessages are never compacted
and are re-cloned on every save. Measured 6.1x smaller per attachment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): route screenshots to the visible tab, resend full-res on retry
Every mounted raw-app editor claimed the runtime's single screenshot slot, so
take_screenshot could capture a background tab's app; ownership now follows the
visible tab and only the owner releases it.
restartGeneration resent displayMessages' images, which became a 384px thumbnail
when the transcript copy was bounded — retries downgraded the model's own input.
Recover the sent parts from the API message instead.
Move modelSupportsVision to modelConfig: it was untestable behind lib.ts's monaco
import chain, and the denylist missed bundled text-only defaults (Groq/Together
Llama 3.3, Foundry Phi-4 and Mistral-Large). Llama 3.2 and Phi-4 split by variant,
so both are matched narrowly. Pinned against the shipped defaultModels.
Decode attachments one at a time and derive the preview from the bounded copy: a
12MP bitmap is ~48MB and the batch was held live at once, decoded twice each.
The attach tooltip claimed nothing is uploaded, which is untrue for images.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): keep images out of text-only turns and bound the queue
The vision gate only dropped the current turn's images, so history's image parts
still went out after a switch to a text-only model and failed the request; strip
the outbound copy instead, leaving history intact for a switch back.
queueMessage had no cap, and each queued send clears the composer for another
eight, so repeated sends stacked an unbounded batch into one message.
Editing a message resent displayMessages' bounded copy, downgrading the model's
own input; retries recovered the full-size one but then re-persisted it at full
resolution. storedImages pairs the API message with its transcript entry so both
paths resend the original and re-persist the bounded copy.
Reserve image slots before awaiting text attachments: the gap left sending
enabled with an image pending, measured ~90ms for a 40-file drop, now ~8ms
regardless of batch size.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): treat deepseek-v4 as text-only
deepseek-v4-pro ships as a bundled default and the gate let images through to
it, so an attachment would fail the turn. DeepSeek's vision line is deepseek-vl.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): drop a rejected image instead of wedging the conversation
A provider that refuses an image leaves it in history, so every later turn
resends it and fails identically: the chat is stuck until the user edits the
message or starts over, and Retry re-sends the same image. The vision gate only
knows the models we ship, so this is the net for the rest. Strip the parts on an
image-related rejection and say so; unrelated failures keep the image.
Verified at the wire that no provider rejects a base64 data URL: anthropic
(source.base64), openai/gpt-4o (input_image), googleai and aws_bedrock/claude
(image_url passthrough) all 200 and read the image.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): match text-only models exactly, from provider API docs
The gate guessed by substring over model names, which answers the wrong
question. What matters is whether a provider's API accepts image parts, not
whether the model can see: DeepSeek V4 ships vision in its chat product that its
API has no content type for, and o3-mini gained vision in ChatGPT the API never
exposed. Neither is inferable from a name.
Substrings also block working models. 'mistral-large' matches Mistral Large 3,
which takes images; 'phi-4' matches Phi-4-multimodal, which does too. A wrong
entry blocks with no override, while a missing one costs a turn and recovers via
the rejection path, so the list is now exact ids only, each backed by a provider
doc.
Verdicts verified against provider API docs rather than recall. Live-checked
where a doc was contradicted: Bedrock's compatibility matrix claims no Anthropic
model is served over chat completions, but it serves images fine.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): stop retry resurrecting a rejected image
The rejection fallback strips the image from history but leaves the bubble's
thumbnail so the user can still see what they sent. storedImages fell back to
that thumbnail when the API message had no parts, so Retry re-attached the very
image the provider had just refused and failed identically — the conversation
stayed wedged through the one control offered to escape it.
Found by retrying in the UI; unit tests, wire tests and four review passes all
missed it, since it only exists between two separate fixes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): harden image rejection recovery and drop-path attachment
* fix(ai-chat): fix image drop race, mid-turn vision gate, retry aliasing
* fix(ai-chat): key vision denylist by provider, flatten alpha before jpeg
* feat(ai-chat): offer take_screenshot on chromium only, ask for one elsewhere
* feat(ai-chat): image-only sends and click-to-expand image previews
* fix(ai-chat): capture screenshots at 2x and expand tool images full-res
* feat(frontend): expandable image previews in composer and result views
* fix(ai-chat): image-only send edge cases from review round
* fix(ai-chat): keep image-only drafts on rollback, track failing model id
* fix(ai-chat): gate rejection recovery on the failing iteration's model
* refactor(ai-chat): record iteration model via onBeforeIteration, trim tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): restore composer draft when beforeSend preflight fails
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): bound cumulative outbound image bytes per request
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): make the image byte bound part-granular so over-cap turns keep a subset
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): evict newest-first within a message in the image byte bound
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): prune over-cap images from stored history, not just requests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): bound history at every save boundary, keep thumbnail pairing across eviction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): slot-align storedImages so the bubble expands the right image after eviction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): match rejection keywords as whole words so provisioning errors keep images
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): match input_image rejections, restore images refused by non-GLOBAL modes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): defer non-GLOBAL image refusal restore past the composer clear
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): persist full tool screenshots for post-reload expansion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(ai-chat): persist chat images out-of-band via blob-store refs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): scope image blobs per chat and stop cap-eviction rotation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): keep blob-cap chronology across drop-oldest compaction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(ai-chat): derive blob eviction from the saved record, not write times
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): serialize chat history DB writes per manager
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): pin queued history writes to the enqueue-time user database
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): delete stale image blobs only after the chat record commits
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): don't double-restore a queued image-only draft on vision refusal
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): label image-only chats and evicted image-only bubbles
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): keep the in-memory chat mirror hydrated for DB-less sessions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): converge the chat mirror to refs after a successful DB commit
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): guard mirror convergence against rewinding newer saves
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): invalidate pending convergences on identity re-init, keep retry image names
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai-chat): bound the screenshot raster before rasterization
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(ai-chat): drop the no-IndexedDB in-memory image fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mcp): apply token scopes consistently across mcp endpoint tools
Endpoint-tool authorization is now shared between single- and
multi-workspace modes, honors the token's script/flow path patterns for
every path-taking tool, and the JWT minted for proxied endpoint calls
carries scopes derived from the caller's own.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(mcp): review nits: restore create_http_request doc, reword comments
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: parse all names in grouped go param declarations
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: bump windmill-parser-wasm-go to 1.761.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>