Commit Graph
45 Commits
Author SHA1 Message Date
1c2667cd55 feat: add wmill user list and document post-suspend approvers context (#11600)
* feat: add wmill user list and document post-suspend approvers context

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

* docs: approvers holds the resume url approver string when set

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

* chore: regenerate cli skills for approvers wording

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

* chore: update ee-repo-ref to acb254ac509aff244849c09e50a8d39aae185138

This commit updates the EE repository reference after PR #852 was merged in windmill-ee-private.

Previous ee-repo-ref: 9cc330f1a8e80bc075a6ad9839966d3b344f8ddd

New ee-repo-ref: acb254ac509aff244849c09e50a8d39aae185138

Automated by sync-ee-ref workflow.

* fix: drop name column from wmill user list, usr has no name

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

* docs: approvalPage resumes record the logged-in user as approver

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-10-08 08:57:26 +02:00
a2e51cdbaa fix: describe exponential retry as multiplier × seconds^attempt (#11546)
* fix: describe exponential retry as multiplier × seconds^attempt in openflow schema

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

* fix: update folded exponential retry description in openapi-deref.yaml

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

* fix: phrase exponential retry summary per n-th retry, matching seconds field

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

* chore: update ee-repo-ref to acb254ac509aff244849c09e50a8d39aae185138

This commit updates the EE repository reference after PR #852 was merged in windmill-ee-private.

Previous ee-repo-ref: 9cc330f1a8e80bc075a6ad9839966d3b344f8ddd

New ee-repo-ref: acb254ac509aff244849c09e50a8d39aae185138

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-10-08 08:56:37 +02:00
03e484e597 fix: let recover: true from the error handler turn nested failures green (#11559)
* feat: let the flow error handler mark the flow as successful

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

* fix: end the flow as a success when an inner error handler recovers

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

* fix: keep the recovery stop to parents that honor it

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

* chore: update ee-repo-ref to cbda28f632d773fd44e9bef8bea36dcb5e35b55d

This commit updates the EE repository reference after PR #847 was merged in windmill-ee-private.

Previous ee-repo-ref: b5a8e3631f657fbb0a7b4b4ef89ddb9ed2c43b1d

New ee-repo-ref: cbda28f632d773fd44e9bef8bea36dcb5e35b55d

Automated by sync-ee-ref workflow.

* fix: keep skip-failure loops going and mask the recovery flag at its source

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

* fix: recover keeps the steps that run and only turns the flow green

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

* fix: only the root flow turns green on a recovered failure

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

* fix: a flow-chaining error keeps the flow failed after a recovery

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-10-07 14:34:28 +00:00
Ruben FiszelandClaude Opus 5.5 046df9e44d feat: run AI decision steps on OpenAI's Decisions API (#11590)
* feat: run AI decision steps on OpenAI's Decisions API

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

* fix: fail refused openai decisions and keep structured criteria

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

* fix: send only user message lists as openai decision messages

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

* fix: omit descriptions the openai decision step does not have

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

* fix: pass only exact openai user messages through as decision input

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

* fix: check the payload of openai decision message parts

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 12:05:04 +00:00
3cba8a7e4c feat: add an AI decision flow step (TypeSafe's Jev, Cloudflare's Clef) (#11474)
* feat: add an AI decision flow step on TypeSafe's Jev

An `aidecision` step asks TypeSafe's Jev typed questions (choice, score,
yes/no) about a state and returns calibrated answers. It is a plain step:
a flow routes on the answers with a Branch to one after it, which the
editor builds and keeps in step with the questions. It can also be an AI
agent tool, where the calling model supplies the state.

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

* fix: read single-quoted branch conditions and flag branches on a removed question

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

* fix: route decision tools by their tag and keep structured questions as JSON

The questions editor writes only on an edit, so opening a step leaves its
value untouched, and questions the cards cannot hold stay in a JSON editor.

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

* fix: re-pick the questions editor mode when the value is replaced from elsewhere

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

* fix: keep the questions JSON editor's text in step with the value

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

* fix: recreate the questions JSON editor when the value is replaced from elsewhere

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

* feat: run AI decisions on Cloudflare's Clef as well as TypeSafe's Jev

A `cloudflare` provider kind reads the hub's `cloudflare` resource (token and
account id), calls the model's Workers AI URL, and unwraps Workers AI's
`result` envelope. Like TypeSafe, it serves decision steps only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* feat: branch from each AI decision question, and align its editor with the brand guidelines

"Branch on this question" now sits in each choice question's card, which also
links to the Branch to one that routes it and flags options it misses or
branches it no longer matches. Labels, errors, cards and icons follow the form
guidelines and the Branch to one's cards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* feat: branch on an AI decision from its graph node

A decision with a choice question nothing branches on yet shows a button beside
its node that inserts the Branch to one, as its question card does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: resolve Cloudflare resources in the AI proxy, and use the design-system button on the graph

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: put the decision_inputs test's doc comment back on it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* chore: regenerate the flow schema in prompts and the chat after the AiDecision description edit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* feat: switch AI decision questions between the form and JSON

JSON is always available; the form is offered whenever its cards can hold the
questions, and a value they cannot hold still opens in JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: show step ids as badges and use the JSON toggle of the run forms

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: quote option names in the AI decision branch alerts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* feat: remove stale branches from the AI decision question card

An empty branch is removed directly; one with steps asks first, naming how many
steps go with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* feat: branch on yes/no AI decision questions, and test a decision step from the AI chat

A yes/no question branches like a choice: "Branch on this question" writes one
"yes" branch on `noul >= 0.5`, with the default as "no", and the helper still
reads it once the threshold is tuned. The AI chat's step test runs a decision
alone as a one-step preview, taking the state the chat passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: record removing several stale branches as one undo step

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: name the typesafe resource type TypeSafe

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: store TypeSafe as the typesafe resource type's display name, not in the word map

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: leave the typesafe and cloudflare resource types to the hub instead of seeding them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* docs(AGENTS): resource types ship through the hub, not migrations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: show the questions JSON format beside the JSON toggle

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: give the questions JSON format in the field's description

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: style the decision's graph branch button like Add branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: show the questions JSON format only in JSON mode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* feat: show each question's branching under the questions JSON editor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: drop the redundant In JSON prefix from the questions format

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: shorten the decision state's description and show an example as its placeholder

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: escape a schema placeholder before the JSON editor renders it as HTML

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* fix: give the decision state's placeholder a text and an object example

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sEUVZVYBjkzkaVkjbFeAL

* chore: update ee-repo-ref to 154cbccb6b8d47bf662b5507882917b5933d5a45

This commit updates the EE repository reference after PR #839 was merged in windmill-ee-private.

Previous ee-repo-ref: eaf38c1388f3cc5bece163404fffa959ede8cae0

New ee-repo-ref: 154cbccb6b8d47bf662b5507882917b5933d5a45

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-10-05 14:23:34 +00:00
e7fc1b2e2e feat: restricted job tokens per script and flow (#11484)
* feat: restricted job tokens (job_token_scopes on scripts and flows)

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

* fix: admit flow-run reads, skip dedicated workers, gate on worker version

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

* fix: keep restricted jobs off flow runners, preserve scopes on rename and promotion

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

* fix: keep restricted jobs off every dedicated handoff, confine progress flow id

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

* fix: exclude restricted runnables from dedicated worker startup

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

* fix: gate restrictions on the release after 1.821.0

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

* fix: store per-job scopes on job_perms instead of v2_job, pin inline runs to the checked version

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

* feat: step-level job_token_scopes for flow steps and agent tools

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: fail closed on perms read errors, refuse restricted queue imports, gate step scopes in previews

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: carry a job's scopes on its completion so a re-run keeps the caller's cap

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: carry a zombie job's scopes into its completion

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: leave a zombie for the next sweep when its scopes cannot be read

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* docs: correct the QueuedJobV2 completion comment

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: validate step scopes in batch flows, fail closed on unvalidated step scopes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: refuse flows with step or tool restrictions at push while an older worker is live

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: apply the step-scope worker gate to flow restarts

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: list the job token toggle with the other step and flow settings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* chore: pin the EE companion merged with EE main

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* perf: skip scope lookups for unrestricted jobs; list job token setting last

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* style: rustfmt scopes tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* fix: confine restricted job tokens to their own run lineage; drop remaining extra lookups

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FHGE3ynoAu6yrgeg4kLwL

* chore: update ee-repo-ref to 259ad3bfeef5285ba80eedc86309b11dca001220

This commit updates the EE repository reference after PR #843 was merged in windmill-ee-private.

Previous ee-repo-ref: 2b77c0225dca441235daf7bf0a06ba968df0c927

New ee-repo-ref: 259ad3bfeef5285ba80eedc86309b11dca001220

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-10-03 09:33:44 +02:00
Ruben Fiszel e952298f84 feat: replace the AI input filling toggle with an additional prompt (#11501)
* feat: replace the AI input filling toggle with an additional prompt for AI

* feat: pass the additional prompt for AI to MCP clients and shared AI guidance

* feat: shrink the run page AI card to a button and a collapsed prompt

* fix: offer writers a way to add a prompt for AI from the run page
2026-10-03 09:33:18 +02:00
hugocasaandClaude Opus 5.5 077709b914 feat: rename agent memory options to On and Legacy, explain each (#11442)
* feat: rename agent memory options to On and Legacy, explain each

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

* fix: default the chat agent example to On memory and align the schema copy

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

* feat: offer Legacy memory only to steps that hold it, explain under the toggle

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

* fix: keep the off memory hint true on the saved-agent editor

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

* feat: state the 128k fallback in the context window description

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:59:16 +02:00
c1b59f70dd feat(ai-agent): add compaction memory that summarizes older context (#10928)
* feat(ai-agent): add autocompacted memory that summarizes older context

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): compact on final-answer turns and count what a turn appended

Address the pre-push review findings on the compaction path:

- A turn the model answers without a tool call left the agent loop on its first
  iteration, so a chat-shaped step never compacted and reloaded the whole
  conversation on every later turn. Compaction now also runs after the loop.
- The trigger measured only the last request, so a single large tool result
  could carry the next one past the window without ever crossing 80%.
- The summarization call re-sent the usage-tracking request shape on endpoints
  the loop had already learned to drop it for.
- The flat 8000-token summary reserve swallowed the whole target on a small
  context window, leaving one message in the tail and summarizing the rest.
- A response cut off inside the <analysis> scratchpad was accepted as a summary.
- The chat-mode memory default was a shared object the step form edited in place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): keep Anthropic prompt counts and compact once per response

Address the first CI review round on the compaction path:

- Anthropic's streaming parser dropped `message_start`, the only event carrying
  the prompt-side counts, so a native Anthropic run reported no input tokens at
  all and compaction fell back to a character estimate.
- A loop that exits without issuing another request — a structured-output turn
  does — reached the post-loop pass still holding the previous measurement and
  compacted a second time, or retried a failure with nothing changed.
- The summarization call inherited the step's `max_completion_tokens`; a low one
  truncates the summary inside its scratchpad, which counts as a failure and
  disables compaction after three of them.
- A fired trigger that found nothing to summarize said nothing.
- Memory already over the window — a lowered `context_window`, or a step moved
  over from `auto` — had no way back, since compaction only ran after an
  accepted request. It now also runs once before the first one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): state the summary's own completion cap and drop the pre-flight pass

- The summarization call asked for no completion cap at all, which is "uncapped"
  only on the OpenAI-shaped providers: Anthropic substitutes 64000, over several
  Claude models' output ceiling, and Bedrock leaves the model's own small default,
  short enough to cut the response off inside its scratchpad. It now asks for the
  reserve the split already set aside, raised to the step's cap when that is larger.
- Compaction no longer runs before the first request. The fallbacks the loop learns
  from a rejection are not known that early, so on exactly the endpoints that need
  them the summarization was malformed by construction: it failed, spent a strike,
  and the first agent request still carried the oversized conversation. A memory
  already past the window is repaired on the turn after a request the endpoint
  accepts, rather than by a pass that cannot succeed there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): ask the summary for exactly the room the split reserved

The split scales its reserve down on a small window while the request asked for
a flat 8000, so the two diverged below an 80k window: on a 4k/8k model the cap
alone exceeded the window and every summarization was refused, and on a 20k one
a full-length summary could land the conversation back over the trigger and
compact its own previous summary on the next response. Both now read one
`summary_reserve_tokens`.

The call also no longer inherits the step's reasoning effort. Every provider
counts thinking against that same budget, so a high-effort model could spend the
whole reserve before writing anything and return a summary cut off inside its
scratchpad; the compaction prompt asks for an `<analysis>` block, which is the
reasoning this call needs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): charge the compaction budget for tools and the system prompt

The tail budget was the whole target, but a request also carries the system
prompt compaction keeps and the tool definitions, which are not in the message
list at all. On a small window those are most of it: a tail sized to the full
target left the next request back over the trigger, compacting again every
response, and the no-usage estimate missed the tool schemas entirely so it could
fail to trigger at all. Both now account for them.

The reserve also gains a floor. It is the summary's output cap as well as the
room the split leaves, and scaled down without one a small window gave a
structured nine-section summary a few hundred tokens — truncated inside its
scratchpad every time, which is discarded, which switches the mode off after
three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): count Gemini's tool-use prompt tokens in an agent step's usage

Gemini splits a tool-using turn's input across `promptTokenCount` and a disjoint
`toolUsePromptTokenCount`, and its thinking apart from `candidatesTokenCount`.
The agent step's parser read only the headline fields, so every tool-using turn
under-reported both — and the compaction trigger, which runs off the reported
prompt, could not see the tool results that grew it. It now goes through the same
helpers the proxy path already used.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): calibrate the compaction estimate against the measured prompt

Two rounds running, the finding was "the character estimate cannot see input X"
— tool schemas, then S3 attachments, which are short paths in the message list
and whole images by the time a provider counts them. Enumerating those is a list
that only grows, so the estimate is now scaled to the one number that is ground
truth: what the provider charged for the last request. Attachments, tokenizer
drift and whatever comes next fall out of that, because the estimate is only
ever used relative to itself.

Also stop the Gemini helpers turning an absent count into `Some(0)`. Downstream,
absent means "fall back to estimating the conversation" while zero reads as an
empty prompt and would hold the trigger below its threshold for the whole run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): charge attachments what they cost and let a heavy short prefix compact

The calibration conserved the conversation's total cost but spread it by
character count, so an attachment — a short S3 path in the message list, a whole
image or PDF once a provider expands it — was charged to the text messages around
it and stayed nearly free in the split. It now carries a nominal cost of its own,
which the calibration corrects a residual on rather than the whole gap.

The four-message minimum also refused exactly the case that fix is for: an
attachment arriving on the first or second turn can pass the trigger before four
removable messages exist, and summarizing even one of them saves most of the
prompt. A prefix worth a quarter of the window is now enough on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): never summarize a prefix holding only a previous summary

The message-count floor was carrying a second job: a fresh summary sits in a one
or two message prefix, so requiring four declined it. The share threshold added
last commit admits it, and a summary is reserve-sized by construction — so the
post-compaction shape could spend one summarization per response swapping a
summary for another the same size, shrinking nothing and losing fidelity each
time. A previous summary no longer counts towards that threshold.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): take the context window from the model and drop the estimate calibration

Brings compaction in line with how the AI session does the same job, which had
already answered these three questions.

- The window is looked up from the model. `MODEL_CONTEXT_WINDOWS` in
  `windmill-ai/src/model_context.rs` mirrors the session's table in
  `copilot/modelConfig.ts`, entry for entry and with the same matching rules;
  each side points at the other, since a model added to one and not the other
  compacts at two different sizes. A step's `context_window` becomes the
  override for what the lookup cannot serve, and chat mode writes none.
- Provider usage is normalized where the provider's quirk is, not at the
  consumer. `TokenUsage::with_cache_beside_input` raises `input_tokens` to the
  whole prompt for Anthropic and Bedrock, which report their cached prefix
  beside it; the OpenAI shape already counts it inside. `prompt_tokens()` is
  then just `input_tokens`, rather than inferring the shape from whether a
  write count is present.
- The estimator is no longer calibrated against the measured prompt. The
  session uses the provider's count when it has one and a chars/4 estimate
  otherwise, with nothing in between, and a tail sized a little wrong only
  compacts again a turn later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* feat(ai-agent): summarize memory down to what the database can store

Without an instance object store, memory is a 100KB database row cut
from its oldest message, the summary included, so compaction on a
mainstream model never got to keep anything across runs. A step that
persists there now runs its post-loop compaction pass against the
smaller of the model's window and the cap at chars/4, about 25k tokens:
the loop keeps the whole window, and what is written is a summary plus a
tail that fits. The run logs when that pass summarizes, and how many
messages the write dropped when one still overshoots.

The editor's storage warning on the option is removed: nothing exposes
the instance storage to it, so it keyed on the workspace S3 setting,
which is unrelated to where memory goes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): get a complete, billed summary out of every provider

Compaction against the real providers turned up four things the stub
could not: Gemini and OpenAI's reasoning models think by default and
bill it against the same cap the summary must fit in, so the
summarization request now asks them for their least (none, low); an
OpenAI Responses call that hits max_output_tokens ends in
response.incomplete, whose usage the parser dropped, so that
summarization went unbilled; a summary that quotes </summary> when it
describes its own instruction was cut off at the quote, on the agent
step and the AI session alike; and the prefix could end on an unanswered
user message, after which the instruction reads as part of that turn
(Anthropic merges the two outright). The tail now starts on a user
message, and both prompts tell the model the instruction is not part of
the conversation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): compact down to half the window, on the agent step and the AI session

The gap between the 80% trigger and the target is what one compaction
buys, and every summarization request carries most of the window. At a
70% target a 128k model summarized about 13k tokens of prefix for a
summary of up to 8k, so each ~100k-token request bought a few turns of
room before the next one re-summarized the previous summary. At 50% the
same request frees about 30k.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): drop the workspace-S3 memory hint and state the database bound in the tooltip

The memory field warned that memory is kept in the database whenever the
workspace had no S3 storage. That setting has no bearing on where memory
goes: the instance object store decides, and nothing exposes it to the
editor. The field's tooltip now describes both memory kinds and states
the database bound unconditionally; the run log says what happened.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): send the summarizer its tool history as text

The summarization request carries no tool definitions, and Bedrock's
Converse API rejects toolUse/toolResult blocks that arrive without them,
so on Bedrock every summarization of a prefix holding a tool call failed
silently until the breaker tripped. The prefix's tool calls and results
now reach the summarizer rendered as text, on the agent step and in the
AI session's compaction, which goes through the same proxy.

Also drops the TokenUsage::prompt_tokens accessor, which had become a
plain read of the normalized input_tokens, and shortens the context
window field's description.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): price attachments from the provider count, bound storage in bytes, effort per pro model

Addresses two Codex rounds and a leftovers audit.

- Attachments were priced at a flat 1500 tokens in the split, so a
  multi-page PDF (tens of thousands of tokens to the provider, a short
  S3 path in the message list) could be kept in the tail or leave no
  prefix worth summarizing. They are now priced from the provider's
  count for the request that carried them, less that request's text,
  with the 1500 floor where nothing was counted.
- The database storage bound measured the provider's token count, but
  the 100KB cap is bytes and repetitive text packs several characters
  per token. The persist pass now measures the serialized conversation.
- The summarizer forced `low` on every reasoning model, which the pro
  variants reject (gpt-5-pro takes only high, gpt-5.2-pro starts at
  medium); they now get no effort.
- Dropped the unused prompt_tokens accessor and its orphaned assert, an
  unused PartialEq, a needlessly public lookup, and fully-qualified
  Gemini calls; refreshed stale comments and the memory_id schema doc;
  regenerated the flow schema artifacts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): evict a heavy attachment into the summarized prefix, not the tail

Pricing attachments from the provider count was not enough on its own: a
leading attachment is a user message, and the boundary rule pulled the
last unanswered user turn back into the kept tail to keep it with its
answer. For a heavy attachment that dragged it into the tail — or, at
the front, emptied the prefix — so it was never summarized and rode
every request. The boundary now moves forward instead, keeping that
user turn and its answer in the summarized prefix. Verified on the
running instance: a 25k-token PDF on a 30k window is summarized out on
the turn it overflows, and later turns drop from 26k to ~1.5k tokens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): keep the forward boundary move off tool results and the prefix start

The forward move that keeps an unanswered user turn out of the tail had
two edges the third Codex round found: advancing past the user could
land the boundary on a tool result (its tool_calls then summarized away,
orphaning it), and with no system prompt the summarizable prefix starts
at 0, so a trigger firing while the tail estimate fit everything indexed
below the start and panicked the task. The forward scan now skips
tool-opening boundaries, and the move is guarded above the prefix start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): drop the step temperature from the summary request

OpenAI's reasoning models (gpt-5-mini, gpt-5.1, gpt-5.2) reject
`temperature` alongside any reasoning effort but their own default, so a
step configured with a temperature made every summarization fail once
the summarizer forced a low effort — history then grew unchecked. The
internal summary call now omits the step's temperature: a structured
extraction does not need a set one, and omitting it sidesteps each
provider's temperature-versus-reasoning rules. Confirmed against the API
that low + temperature is refused on those models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): compact an oversized loaded memory before the first request

Compaction was reactive, taken only after a request the endpoint
accepted, so the fallbacks the loop learns from a rejection are known
first. But a memory loaded from an earlier run can already exceed this
run's window — the step was switched to a smaller model, or a run under
a wider one persisted more than fits — and that first request then
overflows and fails the run, with every retry reloading the same
history and failing again. A pass is now taken up front, off the
character estimate, before the first request. It uses the default
request shape; an endpoint needing a fallback may reject this one
summary, which is non-fatal, and mainstream providers need none.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): under the storage bound, trigger on the max of bytes and model tokens

The storage-bound pass measured only the serialized row size, so an
attachment — a few bytes as an S3 path but nearly the whole model
context — read as tiny and the pass skipped a compaction the model
needed. It now takes the larger of the byte measure and the model's
token count, since repetitive text is few tokens but many bytes and an
attachment is the reverse; either being over must fire a pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): drop oldest turns when a summary cannot fit the window, as the AI session does

An oversized loaded memory (a step switched to a smaller model, or an
object-store run that persisted more than a later model's window holds)
left a prefix larger than the summarizer's own window, so the summary
request overflowed and failed, the memory was untouched, and every
retry failed the same way. The AI session handles this by falling back
from summarization to dropping the oldest turns down to the target;
compaction here now does the same. When a summary cannot run — it
failed, the breaker is tripped, or nothing is worth folding — the oldest
turns are dropped until the conversation fits and opens on a user
message, keeping the newest turn. The next request then always fits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): drop whole turns only, keep the storage pass to bytes, refresh the count after a rewrite

Three edges the seventh Codex round found, all in the drop-oldest
fallback and the storage-bound measure:

- drop_oldest_to_fit dropped to any point that freed enough, which could
  strand a tool result whose tool_calls went with the messages before
  it. It now drops whole turns only, always landing the boundary on a
  user message and never splitting the newest turn; a lone turn too big
  for the window is left whole rather than broken.
- The storage-bound pass measured the whole model prompt against the
  shrunk 25k window, so a large tool roster and the system prompt —
  neither written to the row — tripped it on a conversation the row
  easily held. It measures the serialized bytes alone now; the model's
  own window is enforced by the in-loop passes and the pre-first-request
  pass, so the persisted size is all this pass is for.
- A compaction rewrites the message list, so the provider's count for
  the request that produced it no longer lines up. The count is now
  cleared after any pass that rewrites the conversation, so a later pass
  measures the estimate over the actual messages instead of a stale,
  larger prompt (which could decline a summary that already fit and then
  drop it). The step temperature, no longer sent to the summarizer on
  any path, is dropped from the request struct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): measure only the persisted messages against the storage cap

Persistence strips the system prompt before writing the memory row, but
the storage pass was serializing every message including it, so a large
system prompt with a tiny conversation reported far over the storage
trigger, and the fallback dropped the one real turn, run after run. The
storage measure now serializes only the non-system messages, matching
what the row actually holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix(ai-agent): run the model-window pass before the storage-bytes pass post-loop

A turn the model answered without a tool call broke before the in-loop
compaction check, so on database-backed memory its only pass was the
storage one, which measures bytes. An attachment fills the model context
but is a few bytes in the row, so that turn never compacted and a
follow-up could overflow the model. The post-loop now runs a
model-window pass first, off the provider's count, then the
storage-bytes pass when the row is smaller than the model — both limits
enforced for a chat-shaped step, not just the one that happens to bind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix: simplify agent compaction and preserve execution history

* fix: remove unused compaction history setting

* fix: preserve answers and recover rejected agent context

* refactor: make agent compaction transactional

* fix: skip agent summaries that cannot fit retained context

* fix: explain skipped agent context compaction

* fix: retain recent agent memory when storage compaction cannot fit

* fix: start retained agent memory at a user turn

* fix: reject unsafe agent memory truncation on storage fallback

* docs: clarify agent context window override scope

* fix: keep recent turns verbatim when compaction memory outgrows storage

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix: keep the compaction summary out of the agent's answers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* fix: shorten the agent context window help text

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH

* chore: update ee-repo-ref to 942d4013f36edac1fc9a9addbdb02198db1c7a05

This commit updates the EE repository reference after PR #812 was merged in windmill-ee-private.

Previous ee-repo-ref: 8ca1682ce6106ba6ea96894fbe606dac64102eb6

New ee-repo-ref: 942d4013f36edac1fc9a9addbdb02198db1c7a05

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-29 19:01:53 +02:00
hugocasa ede4103b00 feat: share AI guidance with the CLI skills and lint flow groups (#11397)
* feat: lint AI agent tool names and flow groups in wmill lint

* refactor: assemble chat and CLI AI guidance from one topic table

* feat: share flow groups, reuse and pipeline guidance with the CLI skills

* feat: share raw app, data table and secret guidance between chat and CLI

* docs: document the shared AI guidance source for contributors

* fix: keep wmill lint running on flows with malformed collections

* fix: reject skill descriptions that are not plain YAML text

* fix: tighten fence typos, script base scope and app prompt order

* fix: skip tool name checks on agent steps linked to a saved agent

* fix: catch any misspelled prompt fence and soften the tool name claim

* fix: align cli eval harness with the files and steps wmill init adds

* fix: drop cli eval checks that expect unrequested deploy commands

* fix: list ansible as mainless and c# Main in script base guidance
2026-09-29 14:47:42 +02:00
hugocasaandClaude Opus 5 c297ed0052 feat: managed memory with an inherited or custom memory id per step (#11118)
* feat: split ai agent memory into agent policy, run memory id and step history

* fix: scope string memory ids to workspace and flow, keep nested tool history inputs

* chore: update sqlx cache for the flow context query

* docs: describe memory id scoping as collision-free rather than isolated

* chore: regenerate openflow json after merging main

* fix: offer no memory id for legacy manual memory, document linked history inputs

* fix: seed provided messages from legacy manual memory and hide its note once set

* fix: bypass memory when a provided messages expression evaluates to null

* fix: require a user message when provided messages are empty

* chore: keep the empty messages comment within the line width

* docs: name the history inputs wherever linked steps list their flow-local inputs

* docs: keep the memory storage path on one line

* feat: managed memory with an inherited or custom memory id per step

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

* fix: list a custom memory id in the test run form and name where an inherited one comes from

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

* fix: keep memory id out of the add-field menu and drop the memory id telemetry

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

* fix: keep legacy auto memory without an id working after an untouched redeploy

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

* fix: rename step messages to previous_messages and address review

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

* style: rewrap comments and docs lines lengthened by the previous_messages rename

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

* refactor: read agent memory as either a legacy shape or the current one

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

* fix: name the memory setting in ignored-input notes and keep conversions honest

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

* fix: keep a legacy memory count unset on open and read a cleared count as off

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: address review on cleared test history and zero-count memory

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: drop flow-local keys from a linked agent resource before interpolating it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: keep a linked resource's own inputs as fallbacks and note ignored history on image runs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: restore the linked agent draft tests and log ignored history on every image run

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: resolve the one-of variant from the value when the selected one leaves the list

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: treat zero-count managed memory as off when enabling chat mode and shorten comments

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: stop requiring user_message in the openflow agent contract when previous messages are the prompt

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:55:43 +02:00
GuilhemandClaude Fable 5.1 189793c2e4 feat: flow chat model picker on a shared model-settings component (#11187)
* refactor: render the session chat model menu from a shared ChatModelSettings config

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat: pick the flow chat's model and thinking from the provider fields the flow exposes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: name only the thinking level the flow run will send on the model button

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

* fix: let the flow chat take a typed model id and keep a shared thinking input editable

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

* fix: promote a flow input to the model button only where its control can edit it

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

* fix: drop any reasoning token the chosen model rejects before a flow chat run

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 09:59:05 +02:00
GuilhemandClaude Fable 5.1 68f2248018 feat: keep flow inputs and seed the agent when chat mode is enabled (#11177)
* feat: keep flow inputs and seed the agent when chat mode is enabled

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: leave a linked agent's memory and streaming to the agent when enabling chat mode

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 09:29:05 +02:00
a78beff743 feat: dynamic AI agent toolsets (#11050)
* feat: dynamic ai agent toolsets, and memory as a step input

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

* fix: address review round 1 on dynamic ai agent toolsets

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

* refactor: tag enabled_tools and drop the memory step input

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

* fix: let an mcp server entry be named by the path the roster shows

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

* fix: keep $res: out of the tool names the enabled tools picker offers

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

* fix: name an mcp server by its bare path on the one side that can hold it

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

* fix: count the enabled tool names that matched nothing instead of logging them

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

* refactor: narrow an agent's roster in one pass, by whole entries

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

* test: pin that an mcp summary is rejected against a name that is not

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

* chore: regenerate the copilot flow schema after the merge

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

* docs: shorten the enabled tools list hint

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

* refactor: take enabled_tools back to a plain list of tool names

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

* docs: keep the enabled tools add-menu hint describing the unset field

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

* fix: name a websearch tool that carries no summary of its own

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

* fix: reserve the name web search is enabled by so no tool can share it

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

* refactor: spell the reserved web search name with a hyphen

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

* refactor: reserve __wm_web_search as the name web search is enabled by

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

* chore: advance ee-repo-ref past the git sync ci check work

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

* docs: shorten the enabled tools description the run form shows

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

* chore: update ee-repo-ref to e4c1b794d6c5e6e390987341b2840587bbb40348

This commit updates the EE repository reference after PR #785 was merged in windmill-ee-private.

Previous ee-repo-ref: af668462f0f06b02a5f4e0c22e6156858487a518

New ee-repo-ref: e4c1b794d6c5e6e390987341b2840587bbb40348

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-09-15 11:34:30 +02:00
63cb46d7bb feat: add a minimal skin for the approval page and slack/teams (#11061)
* feat: add an approval skin to the approval page and slack/teams messages

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* chore: point ee-repo-ref at the teams approval skin commit

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* fix: resolve the approval skin from the step awaiting approval

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* fix: shorten the slack approval message to fit the button value limit

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* fix: rename skins to detailed/minimal and keep long slack messages

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* feat: title the minimal approval page from the step and flow summaries

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* feat: let wait_for_approval set the description approvers see

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* fix: keep a finished workflow's approval description, still gated

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* fix: keep a login-required approval locked after the run moves on

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* feat: hide the windmill version on the approval page

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9

* chore: update ee-repo-ref to e92abc9d1fba3ba898640df0cfeb0af8a50849b4

This commit updates the EE repository reference after PR #786 was merged in windmill-ee-private.

Previous ee-repo-ref: bf1766ff49458f62d3f11746f1a06893ae3c2325

New ee-repo-ref: e92abc9d1fba3ba898640df0cfeb0af8a50849b4

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-09-10 18:50:09 +00:00
GuilhemandClaude Opus 5 2b4369d7cb fix: reject invalid AI agent tool names when the chat writes a flow (#10756)
* fix: reject invalid AI agent tool names when the chat writes a flow

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

* fix: address review nits on agent tool name validation

Share one AI-agent walk between the providerless-agent and invalid-tool-name
collectors, drop the unused validateToolName, and list every reserved id in the
tool naming rules.

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

* fix: describe an agent tool's summary as the name the agent calls it by

The OpenFlow schema described `AgentTool.summary` as a short description of
the tool, which is the same schema the flow write tools hand the model, so it
pulled against the naming rules. Narrow those rules to flowmodule tools, since
websearch and mcp tool names are never regex-checked, and let `kind` take
either vocabulary its callers resolve.

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

* fix: name-check only the agent tools whose summary the agent calls

An mcp tool exposes the MCP server's own tool names and a websearch tool's
summary is a plain label, so neither reaches the worker's name check. Both
default to an empty summary in the editor, which the chat then refused to
write back.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 10:49:01 +02:00
fb82748296 fix: make on_behalf_of control permissions for scripts and flows (#10438)
* fix: make on_behalf_of control permissions for scripts and flows

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

* fix: inherit the recorded on-behalf-of identity when a preserving deploy omits it

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

* fix: keep an omitted permissioned_as from re-versioning an unchanged script

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

* fix: derive the on-behalf-of principal from the email and reject mismatched pairs

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

* fix: stop workspace deploys from carrying a source-workspace principal

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

* docs: correct the onBehalfOfPermissionedAs param doc

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

* test: pin that workspace deploys never carry a source-workspace principal

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

* docs: correct the omitted-principal contract and refresh generated prompts

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

* fix: keep external-superadmin principals on email-only redeploys

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

* fix: scope the recorded principal to its workspace and prefer real accounts

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

* fix: carry the recorded principal correctly through drafts and set-permissioned-as

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

* fix: sweep draft identity pairs on email change and offboarding

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

* fix: leave group identities alone when sweeping a user's email

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

* fix: treat only g/ without an email as a group, and match the offboard preview

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

* fix: stop the group guard from skipping rows with no recorded principal

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

* docs: state the group guard once instead of restating it

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

* refactor: make the permissioned_as the only stored on-behalf-of identity

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

* perf: skip resolving the on-behalf-of address for sync clients that discard it

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

* fix: address the local review of the identity refactor

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

* fix: resolve the on-behalf-of identity coherently across clones, offboarding and no-op deploys

* test: pin that a fork keeps only the on-behalf-of identities that resolve in it

* fix: decide a principal prefix-first everywhere and canonicalize bare addresses

* fix: prefix a slash-containing address so a reader cannot take it for a group

* fix: read an address as a username before the group- convention

* fix: rewrite the canonical principal when an account's address moves

* fix: keep the address form of a principal to accounts without a usr row

* fix: reject an identity a job row cannot carry and read it uncached at dispatch

* fix: count characters against the job identity width and cap the backfill

* refactor: name the script/flow principal on_behalf_of, as apps do

* docs: state the caller-must-authorize contract on the identity resolvers

* fix: keep writing on_behalf_of_email until every worker reads the principal

* fix: err high on the compatibility version and document the last resolver

* fix: keep the compatibility address current through identity mutations

* fix: carry the compatibility address with the principal on every copy path

* chore: re-pin the EE ref to the companion branch merged with EE main

* fix: key the dbt retry lookup on the stored principal

* fix: keep a mixed-version address recoverable through a fork

* fix: read a round-tripped address uncached so a redeploy is not rejected

* fix: refuse an email change that would make a principal unenqueueable

* chore: update ee-repo-ref to ac3d7d015296f041ae44ab6bc4953485f44d36e4

This commit updates the EE repository reference after PR #704 was merged in windmill-ee-private.

Previous ee-repo-ref: 219b0b03905a1a0028054b3a4985724e77d09036

New ee-repo-ref: ac3d7d015296f041ae44ab6bc4953485f44d36e4

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-08-01 20:37:21 +02:00
Ruben FiszelandClaude Opus 5 032300e28e feat: run dbt projects as a first-class Windmill runtime (#10326)
* fix: mount only the engine in the dbt jail, reject shadowed and malformed args

Review round 42.

The jail mounted the whole dbt cache directory, whose siblings of the
engine are `repos/` and `packages/` — other workspaces' private checkouts
and package trees, kept apart by cache key rather than by permissions. A
jailed project could read them. It now mounts the engine's own directory,
which the provisioner names; verified from inside the jail that `repos/`,
`packages/` and `state/` are invisible while the engine stays usable.

A `{{ placeholder }}` may no longer take the name of a run argument this
runtime defines. It was silently dropped from the signature, so a
descriptor like `value: "{{ select }}"` deployed and then could not be
run at all: the built-in `select` is an array and the interpolation needs
a scalar. Refused at parse, so the deploy says so.

A `vars` override that is not an object is refused rather than ignored.
Argument-schema validation is opt-in, so a string or an array silently
ran the descriptor's own vars — against a different schema or alias than
the caller asked for. `select` and `exclude` already refused theirs.

* feat(dbt): the project is the script's module bundle, not a git checkout

A dbt script now carries its whole dbt project as its module bundle. The
descriptor is the script content; `<script>__dbt/` holds the project verbatim,
so importing an existing project is `cp -r` plus `wmill sync push`, and the
worker materialises the bundle into the job directory instead of cloning.

Backend
- `prepare_project` writes the script's modules and requires `dbt_project.yml`
  at the bundle root. `checkout`, the git-ssh command, the clone cache and the
  repository resource are gone, along with `repo`, `project`, `ref` and
  `git_ssh_identity` on the descriptor.
- Run identity and the package cache key take a `project_digest` (sorted SHA256
  over the bundle) where the commit used to sit, so an edited project cannot
  resume a previous run's `run_results.json` or reuse its `dbt_packages`.
- The per-run graph re-ingest is now gated on `vars` placeholders and `$var:`
  env alone.
- `capture_dependency_job` takes the script's modules so a dependency job, which
  has no generic module-writing step, materialises them itself.
- `dbt deps` caching strips the git remote from every package it cached, not
  just the tree root: `packages.yml` can render a token into a `git:` URL.
- `git_clone.rs` is dropped and `ansible_executor.rs` returns to its own copy of
  the clone helpers.

CLI
- `wmill sync pull` keeps a dbt script's lock beside its folder rather than
  inside it, so the folder holds nothing but the project.
- Directories dbt generates (`target-path`, `packages-install-path`,
  `clean-targets` and the usual defaults, read from `dbt_project.yml`) are
  excluded from the bundle, from the sync diff and from staleness hashing.
- A module-only edit now pushes its parent dbt script and is reported as a
  changed module rather than passing unnoticed.

* fix: keep a script's modules in the worker's file-system cache

The first fetch of a script version reads the database and carries its
modules; every later fetch imports from the worker's cache directory, whose
`RawScript::import` hard-coded `modules: None` and whose `export` never wrote
them. A worker restart therefore started running the script without its own
files, silently — for a dbt script, without its project, which fails with
"carries no project"; for any other script with a module bundle, with the
imports missing.

`modules.json` is now written on every export and required on import, so an
entry written by an older version fails to import and is refetched rather than
serving a stripped script for as long as the directory lives.

Also derives a dbt run's `project_digest` from the bundle the run actually
carries: `handle_dbt_job` was passing `None`, which collapsed every project in
a workspace onto one digest and let `dbt retry` resume a different project's
`run_results.json`.

* fix(dbt): give every phase the script's environment, bound the cache copies

`dbt deps` ran without the script's environment variables on an unsandboxed
worker, so a `packages.yml` resolving a private package URL through
`env_var()` could not see them while the package cache key was still built on
their digest. `with_invocation_env`, applied at three of the four call sites,
is folded into `dbt_command` so no phase can be added without it, and
`DBT_TARGET_PATH` is set after both environments rather than before.

The package cache copies ran through a bare `Command::output()`: the tree is
the project's, so a cancelled or timed-out job held its worker slot until `cp`
finished. Both the restore and the publish now run under the job poller like
every other phase.

* fix(dbt): only offer commands whose writes match the graph, honour packages-install-path

`dbt_command: run` is dropped from the allowed overrides. Asset dispatch fires a
script's deploy-time writes on any successful job, and `dbt run` covers models
only, so a project with seeds or snapshots notified consumers of relations the
invocation left stale. That is the same reason `test` was already excluded.
Narrowing what a run touches is `select`/`exclude`, which scope the graph too.

`dbt deps` writes to the project's `packages-install-path`, so a project that
moved it got no package cache at all: the publish found nothing at
`dbt_packages` and every job resolved its dependencies over the network again.
The path is read from `dbt_project.yml` and validated as project-relative,
since both cache copies are rooted at it.

Also states the sidecar's mutator contract at the module level: the dbt manifest
tables carry no RLS and grant `windmill_user` full access, so a user-scoped
transaction is not enforcement and every caller must have verified write access
to the script itself.

CLI: a module file is now grouped with its parent script for the push. Left in
a group of its own it got its own `alreadySynced`, so a push touching several
files of one bundle deployed the script once per file; the resulting versions
raced, and the asset graph could end up describing none of them.

* fix(dbt): seed a project for browser-created scripts, refuse a no-op retry

A dbt script created in the browser only got a descriptor, and the runtime
refuses a script whose bundle has no `dbt_project.yml`, so the advertised
Create → Deploy → Run path always failed its dependency job. New dbt scripts
now start with a project that builds: pointing `profile.resource` at a
warehouse is the one edit, and growing it is `wmill sync pull` plus a local
editor, which is where dbt development happens.

`dbt retry` builds its graph from the previous run's error, fail and skipped
nodes alone, so retrying an all-green run selected nothing and wrote nothing —
and a job that succeeds having written nothing still dispatches every
deploy-time write, waking every downstream consumer for relations no one
touched. Refused, with the reason.

CLI: a configured `target-path` or `packages-install-path` may be nested
(`build/target`), and `clean-targets` has a block form as well as an inline
one. Both are now parsed, and the exclusion compares the project-relative path
rather than the top-level segment, so a nested generated tree no longer lands
in the bundle and no longer makes a local `dbt run` look like a project change.

* fix(dbt): lock a project once, find the parent on either path separator

A dbt script's modules are its dbt project, not helper code with dependencies
of its own, so the generic per-module lock loop is skipped for it: the parent
lock already ran `dbt deps` and `dbt parse` over the whole project. Locking
each file separately re-materialised the bundle and re-invoked dbt once per
file, so a project of N files paid N project-sized passes and a large one timed
the deploy out. The 13-file fixture went from 14 relock passes to 1.

`pushParentScriptForModule` searched the raw path for `__dbt/`, so on Windows,
where the folder is spelled `__dbt\`, a module-only edit returned without
deploying its parent while the caller still recorded the file as synced. It now
goes through `getScriptBasePathFromModulePath`, which normalizes separators.

Also drops the last of the external-repository wording from the descriptor's
module docs and from the `codebase` rejection a user can hit.

* feat(dbt): infer the run form locally, keep test-only retries from cascading

`windmill-parser-wasm-yaml` 1.770.0 carries `parse_dbt`, so the browser and the
CLI derive a dbt script's run arguments from its descriptor instead of waiting
for the deploy to hand back a schema. Pins bumped in both.

`generate-metadata` was rewriting a dbt script's `lock` field on every run: a
dbt lock comes from the dependency job on a worker, so nothing generates it
locally and the resolved `!inline` reference was left inlined into the metadata
or blanked. It is restored instead, and a push straight after
`generate-metadata` is a no-op again.

A retry now needs a failed node that materialises something. `dbt retry` builds
its graph from error, fail and skipped nodes, and with `test_behavior:
after_all` a failing test is what `run_results.json` ends up describing — so the
retry reran tests, wrote nothing, succeeded, and still dispatched every
deploy-time write.

The dbt badge's destination is deterministic: writers outrank readers, and among
several writers of one relation (which the backend permits) the smallest id
wins, rather than whichever write edge arrived last.

* feat(dbt): browse the project and read a run's per-node result

Two views a dbt user expects and that the generic script surfaces do not give.

**The project.** A dbt script's editor gains a Project tab beside its
descriptor: the module bundle as the tree dbt itself expects, each file
read-only with syntax highlighting. The existing module tab strip is a flat row
built for a couple of helper files and does not survive a real project; a
13-file fixture already overflows it. Directories sort before files so it reads
like the checkout on disk, and an empty bundle explains the `cp -r` instead of
showing a blank pane.

**The run.** `DisplayResult` renders a dbt invocation's per-node breakdown above
the raw payload: totals, then a table of node, kind, target relation, rows and
time, with failures and warnings sorted first and carrying their message. The
data was already structured; it was being shown as JSON to scroll and PASS/WARN
counts to find in the log. On a failed run the same JSON rides in the error
message after the exit-status line, so it is parsed back out — that is the case
worth rendering, since the failing node is what the user came for.

* docs(dbt): say that profile.resource is what buys the asset graph

The starter descriptor described `profile.resource` as the thing rendered into
profiles.yml, with the project's own file as an equal alternative. It is not
equal: the resource PATH is the warehouse's identity in the asset graph, so a
project bringing its own profiles.yml runs fine and silently gets no assets, no
lineage and no cascade. The deploy already says so in its log; now the
descriptor a user starts from says it too, before they choose.

* fix(dbt): authorize a resource used only for asset identity, clean up after failed installs

A descriptor setting both `profile.profiles_yml` and `profile.resource` took
its connection from the project's file but returned the resource path as the
graph's warehouse identity without ever reading it. A script editor could
therefore publish `table://<any resource>/...` writes, and wake that
warehouse's subscribers, while connecting somewhere else. The resource is now
read on that path too — reading is what authorizes it — so the combination
keeps working for the case that wants it (keep your own profiles.yml, still get
lineage) and fails closed otherwise.

Provisioning cleaned up its staging directory only on the paths someone
remembered, so a run of failed or cancelled first-use installs accumulated
venvs, tarballs and installer scripts until the worker's disk was gone. All
three engines now hold their scratch paths in a guard that removes them on
drop, which is the one exit every path takes, cancellation included.

Frontend: `partial success` is dbt's word for a node that built but whose tests
failed, counted in `totals.error` and redone by a retry, so it ranks with the
failures instead of rendering green with its message hidden. And the run panel
now keys off the worker's engine discriminator rather than `{nodes, totals}`,
which is a shape an ordinary script can return. Both pinned by unit tests on
the extracted `parseDbtRun` helpers.

* feat(dbt): show a run's models on the run page

The run page is where you land on a running job, and until now it showed a dbt
run as streaming text: the per-node table only renders once the job has
produced a result, and the graph that moves per model lived on the pipeline
page you had to navigate to. A Models section now sits above the result,
scoped to the running script's own relations and its `ref()` lineage, polling
while the job is in flight so nodes move as dbt walks the DAG.

No `resolveGraph`: that merges drafts and live editor buffers into the
persisted graph, and a run page has neither.

* feat(dbt): retry failed nodes automatically, and from any worker

**Node-level retry, in the job.** `retry_failed_nodes: {attempts, delay_seconds}`
rebuilds only what a failed build left failed or skipped, before the job reports
failure. dbt confines a failure to its own subtree and `dbt retry` resumes
exactly that set, so a transient warehouse error costs those nodes rather than
the project. Doing it in-job is what keeps the state question out of it: the
previous attempt's `run_results.json` is still in the job directory, so there is
nothing to persist and no worker to land back on. This is the granularity
astronomer-cosmos gets from one Airflow task per model, without the ~6x that
per-model tasks measured.

A retry's `run_results.json` names only the nodes it redid, so it overlays the
accumulated results rather than replacing them: the job's result has to be every
node the job touched, or the nodes that succeeded before the retry settle no
materializations. Pinned by a test.

**Durable retry state.** `run_results.json` is now saved to `dbt_run_state` as
well as the worker's local cache, so an explicit `dbt_command: retry` works from
any worker of the group rather than only the one that failed. Only the results
are stored: `dbt retry` also needs `manifest.json`, roughly sixty times larger
and growing with the project (732 KB against 12 KB on the six-node fixture), but
the manifest is a pure function of the project files, vars and env, all of which
the stored identity already pins, so a worker restoring from the database
re-derives it with a `dbt parse` of about a second.

* fix(dbt): restore the sqlx cache, make retries cancellable and path-aware

**SQLx cache.** A `cargo sqlx prepare` deleted 750 entries, including the
enterprise queries CI needs under `SQLX_OFFLINE=true`, and the check that was
supposed to catch it reported zero losses because it was run from `backend/`
with a `backend/`-prefixed path, so its baseline was empty and it failed open.
All 750 are restored; the branch now adds 19 and deletes none, and
`SQLX_OFFLINE=true cargo check` passes.

**Retry backoff observes cancellation.** `canceled_by` is only written by the
job poller, which does not run between attempts, so re-reading it reported the
state as of the failed attempt and missed every cancel issued during the wait
— the whole window the check exists to cover. The wait now reads
`v2_job_queue.canceled_by` each second, and the job's deadline is honoured
before starting another dbt process.

**Retry state follows its script.** `dbt_run_state` is path-keyed like the
manifest sidecar but, unlike it, nothing regenerates it: a rename moves the row
so a resumable failure survives, while archive and delete clear it, so a script
later created at that path cannot inherit a stranger's failure and its
arguments.

**CLI.** `table` joins ducklake and s3object in the local graph's auto-trigger
kinds, matching `is_auto_trigger_kind` and the frontend's set; without it a
local graph and the generated docs omitted a cascade edge the deploy has.

* fix(dbt): carry only the project files a bundle can hold, and say what it drops

Exploring real and edge-case projects surfaced three frictions, all in the
import path a user hits first.

**A binary file broke the push, opaquely.** dbt projects carry images under
`docs/`, stray `.DS_Store` files and occasionally a parquet seed. Read as text
they become mojibake, and a NUL among them is rejected by Postgres with
`unsupported Unicode escape sequence` — which `wmill sync push` then reported as
success, exiting 0 with the script never created. Binary files are now detected
the way `git` detects them, by a NUL in the first 8000 bytes rather than by
extension, and skipped with the reason.

**The size guard the docs promised did not exist.** Now it does: 5 MB per file,
which only ever catches a committed dataset. Real dbt code is about 500 bytes
median and 1.9 KB at p90.

**Skipped files became a permanent phantom diff.** The push dropped them while
the sync diff still offered them, so every push reported changes no push could
resolve. One predicate now answers for the push, the staleness hash and the
diff.

Verified on a project with unicode filenames and content, CRLF endings, an
empty model, an ephemeral model, a disabled model, a `.md` docs block, an
extensionless README, six levels of nesting, a 7.6 MB seed and a PNG: it
pushes, round-trips byte-for-byte through pull, deploys to 7 dbt nodes and 6
`table://` assets (ephemeral and disabled correctly absent), and runs green.

* fix(dbt): resolve dbt-core against the adapter, settle partial success, unify status

**Adapters could not be provisioned.** The 1.x engine pinned `dbt-core` to a
fixed version independent of the adapter, but several adapters cap below it:
`dbt-mysql` at `~=1.7`, `dbt-oracle` and `dbt-databricks` below 1.12, and
`dbt-salesforce` has no package at all (it exists only inside Fusion). Those
projects failed at provisioning with a uv resolver dump. The install now asks
for a range and lets the adapter choose, and records what the resolver picked so
the lock pins a version that adapter can take.

The floor is the CLI this runtime invokes: resolving down to dbt-core 1.7
produced a working venv that then failed with `No such option '--target'`, which
is worse than not resolving. An adapter with no release in range now fails
naming itself and pointing at `dbt-core-2x` or `fusion`, instead of a resolver
dump. Salesforce is refused up front with the reason.

**`partial success` left a model stuck on `Running`.** It is dbt's word for a
node that built and then failed its tests, and it was already treated as a
failure when counting totals and deciding a retry — but the two sites that
settle the RELATION fell through to "says nothing", so the tailer's `Running`
was never replaced and a finished job showed a model still building. Six status
comparisons had drifted apart, two folding case and four not, while dbt-core 1.x
echoes the author's casing and 2.x uppercases; they are now one classifier.

**Agent workers.** The durable retry state and the cancellation poll both need a
database, which an agent worker reaches only through the API. The automatic node
retry is refused there rather than running a wait it could not interrupt, and
the docs say "any worker with a database connection" instead of overclaiming.

Also clears `dbt_run_state` when a path stops being a dbt script, and moves
`run_identity`'s contract onto `run_identity` from the digest helper below it.

* feat(dbt): show the transform behind a model on the run graph

The run page's graph carried a node for the script itself and drew every
relation as a bare table. Both were wrong for that page: the graph there is
already scoped to one script, so a node standing for it distinguishes nothing
(on the pipeline page it separates one project from another, which is why it
exists), and dbt's own DAG node is the model — the SQL and the relation it
writes are one thing, so a graph of relations alone leaves out what a reader
came to see.

The script node is dropped, and selecting a model now shows its SQL underneath
the canvas with its file path and materialization, read-only, the same view the
pipeline details pane gives.

* feat(dbt): move the graph with the run

The worker has always recorded a state per relation as dbt walks the DAG —
`running` when a model starts, `materialized` or `failed` when it ends — but
nothing rendered it: the graph response carries what a relation IS, not what a
particular run is doing to it, so the canvas had nothing to show and a running
job looked identical to a finished one.

`assets/run_progress/{job_id}` returns that state for one job, the run page
polls it beside the graph, and the asset node carries a spinner or its outcome.
Errors and retries need nothing extra: a failed node writes `failed`, and an
in-job retry rewrites the same row, so the node returns to `running` and on to
its new outcome by itself.

`materialized_partition` holds a relation's CURRENT state keyed by relation, so
filtering on `job_id` returns exactly what this run last touched — which is the
question a run page asks, and why a superseded older run shows nothing.

* feat(dbt): a dbt project is not a data pipeline

Deploying a dbt script marked it `auto_kind = 'pipeline'`, which enrolled it
in pipeline membership: the folder became a Pipeline entry on the home page,
the script folded into it, and `/pipeline/<folder>` opened a canvas holding
the project's whole model DAG next to the pipeline's own scripts. A folder
holding both then read as two projects in one editor, and the pipeline editor
offered to author transforms that are in fact authored in a local `dbt run`
loop and pushed as the script's bundle.

A dbt script is now never a pipeline member, and the pipeline canvas drops the
dbt script node. Its models stay, with their `ref()` lineage: the relations are
what a downstream pipeline script reads, and dropping them would break the
cascade from a dbt run — the point of giving dbt models `table://` identity.

Also drops a screenshot committed to this branch by accident.

* fix(dbt): authorize run_progress through the job, drop dbt from the local graph

`run_progress` read `materialized_partition` through `user_db` on the
assumption that RLS would scope the rows. That table has RLS disabled and no
policies, so any workspace member could pass a job id and read that run's
relation paths, row counts and error text. It now joins `v2_job`, which does
carry per-user policies, so a caller who cannot see the job sees nothing —
the same pattern `v2_job_completed` reads need. Verified as a plain member:
the old query returned 6 rows for another user's run, the new one returns 0,
while the job's owner still sees all 6.

The CLI's local graph still forced `in_pipeline` on every dbt script, so
`pipeline docs --local` and `pipeline dev` kept presenting a dbt project as a
pipeline the deploy no longer enrolls. It now skips them, matching the server.
A dbt descriptor has no asset parser locally, so nothing is lost: its models
come from the manifest the deploy derives.

Declares `run_progress` in openapi.yaml so the frontend uses the generated
client instead of a handwritten fetch; the generated `status` union also
replaces a hand-rolled string mapping.

* fix(dbt): drop the dbt node from the CLI's deployed pipeline views too

`pipeline dev` and `pipeline docs` (without `--local`) read `/assets/graph`
directly. That endpoint is asset-usage driven rather than membership driven, so
it returns a dbt script like any producer — and both commands render every
runnable, so a dbt project still showed up as a pipeline script there after the
local builder stopped emitting one.

`hideDbtRunnables` mirrors the frontend's projection of the same payload. It is
generic over the graph shape so the bounded-cascade view (`BCGraph`, a narrower
type over identical JSON) passes through without a cast.

The relations stay: they are what a downstream pipeline script reads, and the
node is what attributes them to a producer for every other consumer of the
endpoint, so the filter belongs in the views rather than the query.

* fix(dbt): narrow a selective run's cascade, settle the finished run graph

Review-round fixes.

A `select`/`exclude` run builds part of the project, but asset dispatch reads
the deploy-time write set for the whole script, so a run selecting one model
woke the subscribers of every other. Dispatch now intersects that set with the
relations the run actually recorded as materialized, scoped to dbt because it is
the only producer whose write set is decided per run. A run that recorded
nothing still dispatches everything, so an agent worker whose reconciliation
failed cascades as before. Verified both ways: `select: [extra_model]` no longer
wakes the `fct_orders` subscriber, and a full run still does.

`hideDbtRunnables` keyed its removal set on path alone while the graph keys
runnables by `(usage_kind, path)`, so a flow sharing a path with a dbt script
lost its node, edges and triggers too. Both copies now key on the pair.

The run graph never took a final reading when a job finished, so the last state
shown was whatever the tick before completion saw. Only `dbt-core-1x` streams
node events; the other engines record every relation during end-of-run
reconciliation, so their finished graph showed nothing until a reload.

`DbtNodeOutcome::Inconclusive` collapsed statuses the tally has to tell apart,
so two sites re-lowercased the status beside the classifier and `no-op` landed
in `totals.error` — a clean run reporting an error in its own result. Split into
Warn / Skipped / NoOp / Unknown so every site falls out of one match; `no-op` is
kept out of the retry set, which dbt spells as error / fail / skipped.

Also: reattach two doc comments to the items they describe, and correct the
engine-distribution table — only dbt-core-2x is baked into the images, 1.x is a
per-adapter venv provisioned on first use, and the default is compiled in rather
than an instance setting.

* fix(dbt): make the model chip inert where its project node is not on the graph

The canvas passed `onDbtSelect` unconditionally, so the chip always rendered
`cursor-pointer` and hover-highlighted — but the owner map is empty on both
graphs this feature added, since the run page carries no runnables and the
pipeline page hides the dbt node. The chip advertised a click that resolved to
nothing. It now takes its handlers only when the relation has an owner on this
graph, so it stays live on the surfaces that do show the project node.

`classify_status` and `DbtNodeOutcome` were `pub` in a private module with no
caller outside the file, unlike every neighbour.

* fix(dbt): take the cascade's write set from the run's own result

The previous narrowing read `materialized_partition`, which was wrong twice.

That table keeps one row per relation and the newest writer takes `job_id`, so
two overlapping runs over the same model erase each other's claim to it: the
earlier job would dispatch a subset of what it built, or none of it.

And an empty row set was read as "recording failed, dispatch everything" when it
is also a real answer. A `select` matching no model, or one resolving to tests
only, exits 0 having built nothing — and then woke every consumer of every model
in the project, which is the opposite of what the narrowing exists to do and is
reachable by a typo in a run argument.

The run now reports the relations it materialized in its own result, which is
immutable and per job. Absent means the producer said nothing (a job from before
the field, a non-dbt producer) and the whole deploy-time set dispatches as
before; present-and-empty means it built nothing and dispatches nothing.

Verified on all three: an unmatched selector builds nothing and wakes nobody, a
selector naming one unsubscribed model wakes nobody, and a full run wakes the
subscriber.

Also indexes `materialized_partition (workspace_id, job_id)` -- the run page
polls that shape every 2s and no existing index leads with `job_id` -- corrects
the selective-cascade section of the design doc, which still described the old
deploy-time behavior, and reattaches `buildLocalPipelineGraph`'s doc comment.

* docs(dbt): attach the CLI JSDoc to its function, correct the index rationale

The `hideDbtRunnables` JSDoc ended up documenting the type declared beneath it —
made while fixing the same mistake one function down.

The migration's comment credited the cascade with a `job_id` lookup that the
same commit replaced with a read of the job's own result. The run page's poll is
the only reader keyed on that column.

* fix(dbt): refuse graph publication for a removed script, allow test-only retries

An archived or hard-deleted script could still republish its graph: the
publication guard filtered `deleted` but not `archived`, and treated a missing
row as "nothing newer exists" rather than "nothing left to publish for". A
dependency job or dynamic run finishing after the removal put the asset,
provenance and subscription rows back with nothing left to clear them.

`dbt_command: retry` refused a run whose only failures were tests, which is
precisely what `test_behavior: after_all` produces. That restriction existed
because a successful job dispatched its whole deploy-time write set, so a
test-only retry would have woken every consumer for relations no one touched —
the cascade now dispatches what the run reports materializing, so it wakes
nobody and the restriction only blocked a legitimate retry.

* fix(dbt): gate run progress behind the job-read check, not RLS alone

The endpoint joined `v2_job` so RLS would decide visibility, which it does — but
`require_job_read_access` adds two things RLS does not: a scoped token's
`if_jobs:filter_tags` restriction, and the app-embed cutoff that stops untrusted
app JS from inheriting the viewer's broader job access. A scoped or embed token
could therefore read relation names, statuses, row counts and errors for jobs
the ordinary job endpoints deny it.

That helper is private to `windmill-api`, which depends on `windmill-api-assets`
rather than the reverse, so the endpoint moves to the job routes instead of the
check being duplicated. It is job-scoped anyway:
`/w/{ws}/assets/run_progress/{job_id}` becomes
`/w/{ws}/jobs/run_progress/{id}`, and the frontend follows the generated client.

* feat(dbt): a dbt run does not trigger downstream runs

dbt orders its own DAG, so a cascade only ever adds one thing: waking a Windmill
script that reads a mart. That edge is narrow, and only half of it can even be
expressed — nothing outside dbt can declare a `table://` write, since
`// materialize` accepts DuckLake targets only, so an ingestion script cannot
wake a dbt project.

Against that, dispatching correctly is not cheap. A run's `select` can build any
subset of the project, so the deploy-time write set is not what ran; using it
wakes consumers of relations the run never touched, and narrowing it needs a
per-job record of what was built. The per-relation state table cannot supply one
(it keeps a single row per relation stamped with the last writer), and the
result field added for it made a run's own output carry the cascade's bookkeeping.

So `asset_dispatch` returns early for `ScriptLang::Dbt`, before the producer
gate. dbt still materializes, records per-model state and publishes its graph:
models, `ref()` lineage and live run progress are unchanged, and a
`# on table://<mart>` reader still renders beside the model it reads. It simply
does not fire. Wiring it up later means deciding what a selective run should
notify, which is the actual work.

Verified: a full run of a 6-model project succeeds and starts nothing, where it
previously triggered its subscriber; the run page still reports all 6 relations
and the folder graph still carries 14 tables and 8 ref() edges.

* fix(dbt): remove the cascade surface, settle stranded models, fix nested __mod

Stopping dispatch left its surface behind. `table://` was still an auto-trigger
kind, `persist_ingest` still derived subscriptions from a manifest's reads, and
the deploy still accepted `# on table://` — so the canvas drew cascade arrows
into scripts nothing could wake. All three are gone: the kind no longer derives,
the ingest only deletes rows earlier versions wrote, and the deploy refuses the
annotation with a message saying why rather than persisting a silent no-op.
`DescriptorTriggers` went with them; every field it parsed was cascade config.

A model marked `running` by the live tailer was never settled when the run did
not finish: reconciliation only revisits nodes `run_results.json` names, and a
cancelled or timed-out run has none for the model in flight, so the finished job
showed a relation building forever. It is now settled on every exit path.
Verified by cancelling a run mid-flight: 3 models `running` before, 3 `failed`
after, none stranded.

`getScriptBasePathFromModulePath` took the first matching suffix rather than the
outermost boundary, so `proj__dbt/models/legacy__mod/a.sql` resolved to
`proj__dbt/models/legacy`. dbt owns its directory names verbatim, so a folder
ending `__mod` is legal inside a project, and a module-only sync would have
looked for a descriptor that is not there and skipped the deploy.

* fix(dbt): colour a finished run's models from its own result

`materialized_partition` keeps one row per relation stamped with whichever job
wrote it last, so reopening a run showed only the models no later run had
touched since — down to none for an old run, which reads as a broken page rather
than as stale data. Reproduced: a 6-model run reported 6 relations, then a second
run rebuilt one shared model and the first reported 5.

A finished run already carries the answer. Its result lists every node with a
status, and the graph carries each asset's dbt `unique_id`, so the two join
directly — no path derivation, nothing stored twice, and nothing a later run can
overwrite. The endpoint stays for the live window, where the result does not
exist yet, and as the fallback for a run that never produced one (cancelled or
killed, whose relations the worker settles in the table instead).

`relationOutcome` mirrors the worker's `classify_status` so the colour drawn over
a record agrees with the record: `warn`, `skipped` and `no-op` leave the relation
untouched and stay uncoloured, as do tests and analyses, which match no asset.

Verified in the browser on the run whose model had been stolen: all six
relations green again, both sources correctly uncoloured.

* docs(dbt): record why only dbt-core 1.x has live per-model progress

`emits_node_events()` reads as "the Rust engines produce no node events", which
is false and would close off the option. They produce exactly the same events;
they put them on the console and ignore `--log-format-file json`, which both
accept. Measured on 2.0.0-alpha.5 and fusion 2.0.0-preview.202: 15 node events
each on stdout, 0 in the file log, for a three-model project.

Taking them means owning the job log's presentation to work around a flag that
is documented and simply unimplemented, so the note records the measurement, the
sample event, and that flipping the predicate is the whole change once either
engine honours it.

* fix(dbt): give HighlightCode a dialect-agnostic sql language

`npm run check` had three errors the fast check does not reach: `"sql"` is not a
value `HighlightCode` accepts. Every SQL dialect it knows maps to one grammar,
but a dbt model is compiled by whichever adapter the project targets, so naming
a dialect would be a guess — `sql` is now a value in its own right.

`langOf` was typed `string` and returned `markdown`, `python` and `text`, none
of which the component accepts either, so a dbt project's YAML and Python files
rendered unhighlighted. It now returns the component's own prop type, which is
what caught them, and `undefined` for what has no grammar rather than a name
that silently means the same thing.

Verified in the project panel: SQL 22 tokens, YAML 27, where YAML was plain.

* fix(dbt): stop failing no-op models, drop table triggers client-side, keep cross-selection edges

The sweep that settles a run's stranded relations was marking `no-op`, `warn`
and `skipped` models FAILED on successful runs: reconciliation reports those
nodes without settling their record, so they were indistinguishable from a model
the run never reached. It now excludes every relation the run accounted for, so
only the genuinely abandoned ones are settled.

`table` was removed from the backend's auto-trigger kinds but left in both
client mirrors, so the editor and `pipeline dev`/`docs` kept drawing cascade
arrows the deploy will not create.

`isModuleEntryPoint` scanned for the first `__mod/`, the same bug its sibling
just had: a `legacy__mod/script.ts` nested in a dbt project — dbt owns those
names verbatim — read as that script's entry point. Both now anchor on the
outermost boundary.

A script selecting a model whose parent another script builds dropped the parent
entirely, so no `dbt_edge` could reach it and the two relations sat on the graph
unconnected. The parent is now kept as an endpoint and recorded as a READ, since
this script does not build it — splitting a project across selections only
composes if the seam still draws.

* fix(dbt): don't double-run after-all tests, count only models a script builds

An `after_all` run whose test phase failed saves a `run_results.json` holding
tests alone. Retrying it reran exactly those tests — and then the test phase ran
the whole suite again, appending a second copy of every result: duplicate ids in
the run table, doubled totals. A retry whose saved results are tests alone IS
the test phase, so the suite is not run after it, and the two phases now merge
by node id rather than concatenating.

Keeping a selection's unselected parents as nodes made them count toward the
`×N` badge, whose tooltip says "materializes N models" — a script selecting one
mart claimed the staging models upstream of it, and the number grew with the
seam. The count now comes from the relations the script writes.

That change also made the cross-selection read block dead, with a comment
asserting the inverse of what now happens; it is removed, and the test that
covered it still passes on the new arm. The test I added landed between a
neighbouring test's comment and its `#[test]`, orphaning the attribute so that
test stopped running.

Two display fixes: the run page no longer shows a relation's SQL when the
provenance belongs to another project that materializes the same relation, and
the editor no longer draws an explicit `# on table://` arrow the deploy refuses.
The starter descriptor no longer promises the removed cascade.

* fix(dbt): clear untouched models, reject unknown descriptor fields

A `no-op` model was left `running` forever on a successful run. The previous
attempt at this stopped the sweep marking such models FAILED but gave them no
terminal state instead, so they simply never settled. Reconciliation now returns
what it settled and what the run reported but did not build, and the two get
opposite treatment: a relation the run left untouched has its row DELETED, which
is what the finished run's own result says about it (`relationOutcome` colours a
`no-op` nothing), so the live and settled views agree; only a relation the run
never reached at all is failed.

The descriptor accepted unknown fields, so `selcet:` was ignored and left an
empty selection — building the whole project — and a misspelled `target` fell
back to the profile's default. It rejects them now. That immediately caught two
of our own test fixtures still passing `repo:`, a field removed with the git
path, which is exactly the class of mistake it exists to stop.

`isDbtModulePath` matched `__dbt/` anywhere in a path, the third site with that
bug: `foo__mod/vendor/x__dbt/a.ts` read as a dbt project file, and the push then
looked for `foo.script.yaml` and could skip the edit.

A verbatim dbt bundle dropped any file named `*.lock` before it reached the
module map, so an authored `uv.lock` never deployed and the unmodified-project
round trip quietly lost it. The exclusion now applies only to `__mod` bundles,
where `.lock` really is the script's own lockfile — in the walker that hashes
modules too, or a change to such a file would not register as one.

Also: `langOf` fell back to `undefined`, which HighlightCode resolves to
TypeScript rather than to no highlighting, so seeds and Markdown were coloured
as code; and four comments still gave the removed cascade as the reason for
sharing an asset node, which is now lineage.

* fix(dbt): retry failed tests too, anchor the last __dbt path check

`retry_failed_nodes` only ran after the model phase, which fails before the
`after_all` test phase exists — so a project whose models built and whose tests
failed got no retry at all, exempting exactly the failure mode that separate
phase produces. The loop is now a function, called after both phases.

`isDbtGeneratedPath` matched `__dbt/` anywhere, the fourth site with that bug:
`foo__mod/vendor/x__dbt/target/a.ts` counted as generated dbt output, so
`ignoreF` excluded an ordinary module file and a module-only edit never deployed
its parent script.

`wmill sync push` still dropped an ADDED or DELETED `.lock` three branches
before the module arm, so the earlier fix only covered a first push: adding a
`uv.lock` to a deployed project was reported as a change forever and never
applied, and deleting one left it deployed. Editing worked, which is what made
the round trip look whole.

Also removes a duplicate `#[test]` that was double-registering a test and
detaching its neighbour's comment, and rewrites seven comments that still gave
the cascade as the reason for behaviour that now serves lineage only.

* fix(dbt): bound the excluded-file read, keep the retry budget job-wide

`isBundledModuleFile` read a file in full before deciding it was too big or
binary, so a project sitting next to a multi-gigabyte parquet seed loaded the
whole thing only to reject it. It now takes the size from `stat` and reads at
most the 8 KB the NUL check needs: a 191 MB file is rejected in 0.0ms at 82 MB
RSS.

Calling the retry helper after both phases gave each its own `attempts` budget,
so a job could spend double what the descriptor asked for — the bound exists
because every attempt is a real dbt invocation holding a worker slot. The budget
is now the job's, spent across whichever phases fail, and the field says so.

Extracting that helper had also placed it between `#[allow(clippy::
too_many_arguments)]` and `run_dbt`, taking the attribute off the 12-argument
function it was written for.

* fix(dbt): actually spend the retry budget

`retry_failed_nodes` looped on `while *remaining > 0` and never decremented it,
so a failing job reissued `dbt retry` — logging "attempt 1 of 3" each time —
until the job's deadline instead of `attempts` times. The decrement existed
briefly and was lost when the function was re-extracted by hand.

Claiming and counting are now one operation, `claim_attempt`, because keeping
them apart is exactly how the bound goes missing: the loop cannot iterate
without spending the budget.

Its test is bounded by its own `for` rather than by the function under test. An
earlier version collected `std::iter::from_fn(|| claim_attempt(..))`, which
against a non-spending `claim_attempt` is an infinite iterator — it allocated
until the machine died. A test for a loop bound must fail an assertion when the
bound regresses, not consume the host: it now reports `[1, 1, 1, …]` against
`[1, 2, 3]` in 0.00s.

* fix(dbt): ask before reading, not after

Bounding `isBundledModuleFile` did nothing for the bundle builder, which read
the whole file into memory and only then asked whether to keep it — so a
multi-gigabyte seed beside a project was still loaded in full just to be
skipped. The predicate is now consulted first, and the read happens only for
files the bundle actually carries.

* feat(dbt): animate the ref() edges feeding the model being built

The nodes moved during a run but the edges did not, so the graph showed where
dbt had got to without showing it flowing there.

Reuses the canvas's existing rule rather than adding a second one: an edge
animates when it touches what is happening. For a pipeline that is the running
script; for dbt the unit of work is the model, so a `ref()` edge animates while
its target builds. Same `animated` field, same visual language, no new styling.

Verified mid-run on a 7-model project: of six `ref()` edges only the two feeding
the model then building were animated, and none once the job finished.

* feat(dbt): show what each model wrote, and say when its SQL is another project's

Three things a reader wanted from the run graph and could not get.

Row counts: the worker already records one per relation and `run_progress`
already returned it, but the graph used only `status` and dropped the number. A
model that built green having emitted zero rows is the failure that looks like a
success, so the count is on the node.

The relation's fully-qualified name, copyable: there is no table browser to open,
so the next best affordance is the exact identifier to paste into a SQL client.
It is parsed with `splitRelation`, which honours quoting the way the worker's
`split_relation` does — splitting on every period renders
`"wh"."analytics.v2"."orders"` as a relation `orders` in a schema `v2`, which
does not exist.

And when two projects materialize one relation, the graph keeps a single
provenance winner, so the losing project's node carries the other's model. The
SQL was already suppressed there — correctly, it is not this run's code — but
silently, which reads as a dead click. It now says so.

* fix(dbt): a finished run's graph is the models it built, not today's project

`/assets/graph` is the current deploy, so an old run's graph drifted with the
project: a model added after it appeared as though the run had built it, and the
older the run the wronger the picture. A finished run's node set now comes from
its own result, which named exactly what it touched.

Sources survive the filter regardless — dbt never lists them in
`run_results.json` because it does not build them, but they are the upstream the
run read, and dropping them would leave the models hanging.

The graph is still the current deploy's, so a model renamed or deleted since
cannot be drawn at all. Rather than a silently shorter graph, the count is
stated above it.

Verified by adding a model after a run: the old run renders 7 models without it,
a fresh run renders 8 with it.

* feat(dbt): preview a model's rows with `dbt show`

There was no way to see the data behind a node — only its SQL and its row count.
`dbt show` selects from a model and returns rows, and every engine ships it, so
the preview needs no adapter code of ours: no connection path, no dialect-correct
quoting, no type coercion for ten warehouses. It runs against the profile the
run already renders.

It is a `dbt_command` rather than a new endpoint, so it inherits the whole job
path — authorization, isolation, cancellation, logs, engine provisioning — and
`limit` joins the run form beside it. That the allowlist can admit it at all is a
consequence of dropping the cascade: while a successful job dispatched its
deploy-time write set, a command that wrote nothing woke every consumer for
relations nothing had touched.

Read-only, and treated as such: no graph republish, no materialization records,
no retry state, no test phase. Captured rather than streamed, like `dbt ls` —
these rows are the result, not commentary, and the job-log writer is what
`NO_LOGS_AT_ALL` discards.

Verified: `{"dbt_command":"show","select":["stg_customers"],"limit":3}` returns
three rows; a preview leaves `materialized_partition` untouched (62 → 62, 0 rows
for the job); `clean` is still refused by the allowlist.

* feat(dbt): preview a model's rows from the graph, and keep our locks out of dbt projects

The run page could show a model's SQL and how many rows it wrote, but not the
data. Selecting a model now offers "Preview rows", which runs the script with
`dbt_command: show` and renders the result as a table.

Explicit rather than on-select: a preview is a job, so it costs a worker slot
and the engine's start-up, and previewing on every click would spend both on
mere navigation. Sources are excluded — dbt shows what a model SELECTs, and a
source is not one.

Also: `updateModuleLocks` was the one module helper that never learned about
verbatim bundles, so it walked a dbt project writing `foo.lock` beside `foo.sql`.
None of those files is a Windmill script needing a lockfile, and the bundle
promises to round-trip the project byte-for-byte — our artifacts have no business
in it.

Verified in the browser: selecting `stg_customers` and previewing returns the
columns `id`/`src` and five rows from the warehouse.

* fix(dbt): keep a run's models when another project owns their provenance

Scoping a finished run's graph to the ids it named dropped relations whose
provenance winner belongs to a different project — so a run of a project sharing
a schema showed 3 of the 6 models it had built. An id that was never this run's
package cannot be judged against its result, so it is kept: the relation IS one
the run wrote, and hiding it understates the run. The same rule applies to the
"no longer in the project" count, which otherwise reported deletions that were
only provenance collisions.

Previews are now cached per model and survive the selection moving. One was
thrown away whenever the reader clicked elsewhere, which for a job costing a
worker slot and an engine start-up meant re-running it to see it again — and the
run continues in the background, so leaving and returning finds the rows there.
The spinner also never span: `startIcon` takes the icon and its classes
separately, so the animation has to be passed alongside.

How long it took is shown with the rows. A preview is a job, and its cost should
not be something the reader has to guess at.

* fix(dbt): resolve argument references, clamp the show limit, flag renamed relations

`handle_dbt_job` cloned `job.args` where every other executor calls
`build_args_map`, so a `$var:` / `$res:` / `$encrypted:` argument reached dbt as
the literal string. A placeholder holding a schema or an `enabled` flag would
then build a different slice of the project than the caller asked for.

`--limit` took any positive i64, and the worker buffers the whole of dbt's
stdout to read the rows out of it — so a caller with only run permission could
make it hold an unbounded allocation. It is clamped to a ceiling now, extracted
as `show_limit` so the bound is pinned by a test rather than inline in an async
function nothing can reach.

And a model keeps its id when its alias or schema changes, so an old run's node
showed today's relation while the run wrote another — the page asserting it had
materialized a table that did not exist yet. The run's result carries the
relation each node actually wrote, so the drift is detectable without a graph
snapshot, and the count is stated above the graph. Rendering the run's own
lineage still needs a per-job snapshot; this stops the page claiming otherwise.

* fix(dbt): stop persisting resolved secrets, bound the preview by bytes

Resolving `$var:` / `$res:` / `$encrypted:` for dbt — added in the previous
commit — meant `save_run_state` wrote the resolved PLAINTEXT into
`dbt_run_state.args` and the worker's `state.json`. The row outlives the job, so
a secret stayed in the database and a later `dbt_command: retry` replayed it
after the grant was revoked or the value rotated. The invocation now carries the
args as submitted alongside the resolved ones, run state persists those, and the
restore path resolves them again under whoever is retrying.

Clamping `--limit` bounded the row COUNT, not the size: one column can hold a
megabyte, so a thousand rows is a thousand megabytes, and `run_capturing`
buffers all of it. The captured output has a byte ceiling now.

`limit` became a built-in argument without joining `RESERVED_ARG_NAMES`, so a
descriptor writing `{{ limit }}` was silently handed the preview control's
default instead of being told the name is taken.

Two display fixes: the relation-drift banner compared a canonicalized (lower
case) asset path against the warehouse's own spelling, so it fired on every
model of every finished Snowflake run; and caching a preview's failure left
`Preview rows` dead for that model until reload.

* feat(dbt): key the graph by script version so a run renders its own project

The dbt graph was keyed by path alone, so a deploy overwrote the only copy and a
run page could only ever show today's project — an older run rendered today's
models, SQL and `ref()` lineage no matter what it had run. My previous attempt
filtered that view to the ids the run named, which stopped it lying but could not
show what was gone: the data no longer existed.

`dbt_node` / `dbt_edge` now carry `script_hash` in their primary key, so each
deployed version keeps its own graph, and the run page passes the version its job
recorded. Per DEPLOY, not per run — ten thousand runs of one version share one
graph — and a composite FK to `script (workspace_id, hash)` with ON DELETE
CASCADE means a version's graph dies with it. Nothing pruned these before,
because there was one copy per path; they would otherwise have accumulated with
no sweep.

Two deploys of one path now write disjoint rows, so the graph can no longer be
lost to a race. `claim_graph_publication` remains only for what is still
path-keyed — the `asset` usage rows — and an older deploy finishing late records
its own graph before declining to touch those, where before it published nothing
at all.

A pinned request is scoped by the version's own nodes rather than by `asset`:
that table describes the current deploy, so scoping through it would filter a
model out of the very run that built it.

Verified end to end: deployed v1 (8 models), ran it, deployed v2 with four models
removed and one rewritten. The old run renders 8 models, 6 ref() edges and v1's
SQL; a new run renders 4 and the v2 rewrite.

* fix(dbt): scope graph cleanup to one version, bound the preview capture

Archive and delete both act on a single `hash`, but the graph cleanup they
called deleted every row for the path. Now that the graph is keyed per
version, archiving an old version erased the live one's models, SQL and
lineage, and nothing repaired it. Both callers have the path in hand, so the
by-hash wrapper is gone and they use the version-scoped clear directly.

`dbt show` checked its 8 MB ceiling after `wait_with_output` had already
buffered everything, so the ceiling could not bound what the worker held.
`run_capturing` now reads both pipes incrementally against a caller-supplied
limit and kills the child on overflow. The read buffers are heap-allocated:
as arrays they were baked into the future, which the job poller boxes several
layers deep, and that overflowed the worker thread's stack — a `dbt show` run
aborted the whole worker process.

A retry's `dbt parse` ran on the arguments as submitted while the build ran on
resolved ones, so a `$var:` shaping the graph parsed verbatim. The parse moves
to the caller, after resolution.

A run that names its own `select`/`exclude` now drops the descriptor's
`selector`: dbt resolves `--selector` instead of `--select`, so passing both
made a preview of one model return another's rows.

Also: log instead of silently swallowing a `modules` column that fails to
deserialize (pre-existing, but for dbt it means running with no project at
all); keep the model SQL reachable once a preview has landed; render which
node the rows came from; stringify object-valued cells; document
`dbt_script_hash` in the OpenAPI spec.

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

* docs: record the per-worktree dev environment and the backend-run check

Three mistakes this guidance would have prevented, each of which cost a cycle:

A worktree has its own database and ports, but AGENTS.md stated the
single-checkout defaults as facts. Pointing `DATABASE_URL` at another
worktree's database makes `cargo sqlx prepare` fail on every query touching a
table your migrations added — and it deletes `.sqlx/` before it fails, so the
cache is gutted rather than merely stale. Starting a backend on the wrong port
leaves the UI up with every call 502ing, which reads as an application bug.
Both values are now discoverable with commands that work as written.

`prepare` is also documented as the wrong tool for a removal-only change: the
cache is already complete for CI, and the only residue is orphaned entries that
can be found by text-matching against the sources without a database.

Nothing told a reader that `cargo check` does not exercise a worker path. A
read buffer declared as an array inside an async block is baked into the
future, and once boxed by the job poller it overflows the worker thread's
stack — compiling and unit-testing clean while aborting the whole worker
process at runtime.

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

* fix(dbt): scope the remaining path-wide reads and clears to one version

Four places still spoke for a whole path after the graph became per-version:

The relation-root drift check read `dbt_node` by path with an unordered
`LIMIT 1`, so with v1 at root A and v2 at root B it could answer with v1's
row, suppress the refresh v2 needed, and leave v2's graph naming relations the
run does not build. It now reads this job's version.

`dbt_dep`'s no-resource branch cleared the path, so a descriptor edited to
bring its own `profiles.yml` emptied every earlier version's graph and with it
every finished run's page. The ownership being given up is the path-keyed
`asset` usages cleared beside it; the graph clear is now this version's.

The graph was inserted before the publication claim checked the version was
still live. Archive and delete only soft-update `script`, so the foreign key
still accepted an in-flight dependency job's rows and the failed claim
committed them — and because pinned queries deliberately serve archived
versions, deleted model SQL became readable again. The write is now gated on a
`FOR UPDATE` liveness check.

`clear_dbt_run_state_by_script_hash` resolved a hash to a path and deleted the
path's saved run. `dbt_run_state` is keyed by path by design — one saved run
per script — so archiving one version discarded the live version's resumable
failure. It clears only once no live version of the path is left; `identity`
already refuses a resume whose project, warehouse or engine moved.

"Preview rows" ran `runScriptByPath` while the SQL beside it was pinned to a
hash, so an old run showed its own SQL over today's rows. Verified end to end:
with v3 deployed, the v2 run's preview runs v2's hash and returns v2's rows.

Also: keep the TAIL of a captured stderr, since dbt prints its summary last;
one `$derived` for the parsed result rather than five; collapse three
near-identical argument accessors onto one generic; fold the single-use
`copy_dir_command` into its caller; and give `parseDbtRun.ts` one status
classifier instead of spelling dbt's failure vocabulary twice.

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

* refactor(dbt): one JobCtx down the executor, one table per adapter

Two changes aimed at the operations this code will keep having: adding a
phase, and adding a warehouse.

`JobCtx` already bundled the five values every phase needs, and nine functions
took it — but the top of the executor threaded the fields apart and rebuilt the
struct at each call, so the same literal appeared eight times and each new
phase meant five more parameters. It is now built once per entry point and
reborrowed. `prepare_project` goes from 21 parameters to 17, `retry_failed_nodes`
from 15 to 11, and `run_dbt` drops below the lint threshold. The two remaining
constructions are the worker boundary, where the pieces genuinely arrive apart.

`DbtAdapter` answered five questions with five parallel matches over the same
eleven variants, plus a sixth list of adapters kept by hand in a test. The
facts now live in one `AdapterSpec` per adapter, reached through one exhaustive
match, so adding a warehouse states its name, driver, package, port, database
key and licensing together and the compiler demands the arm. Each arm spreads
from a Postgres base, which makes the inheritance visible per adapter instead
of hidden in the `_ =>` defaults `default_port` and `database_key` used to
carry. `DbtAdapter::ALL` replaces the list the test kept separately.

Verified by dumping all seven facts for all eleven adapters before and after:
byte-identical.

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

* fix(dbt): job-keyed run progress, and stop path-wide reads and clears

Six findings from the last round, in the order they bite.

The publication liveness gate refused on `archived`, but `create_script`
archives the parent on every redeploy — so deploying v2 while v1's dependency
job was still parsing left v1 without a graph, permanently, which is the exact
case the unconditional write existed to serve. It gates on `deleted` alone now;
an explicit archive is still covered by the `FOR UPDATE` ordering.

A project-owned `profiles.yml` trusted `profile.type` instead of reading the
file. The Rust engines carry every adapter, so a CE script could declare
`postgres` over a target that is `sqlserver` and have dbt connect with the
enterprise adapter. The file is read whichever way, and a descriptor that
disagrees with it is refused.

Renaming a dbt script, or editing one so its newest version is no longer dbt,
cleared the graph for the whole path — every older version's models, SQL and
lineage, which their own finished runs still render. Neither needs it: graph
queries join on `(path, hash)` through a `language = 'dbt'` CTE, so an old
version's rows cannot attach to whatever lives at that path next.

Live progress read `materialized_partition`, whose key is the relation and
whose `job_id` is only the last writer. Two runs of one project took rows from
each other. Progress now has its own job-keyed table; the relation table is
untouched, because one row per relation is right for the pipeline canvas and
fork defer. Verified with two overlapping builds: both keep 6 rows in the new
table, while the old one attributes 6 to one run and 0 to the other.

`Scratch::drop` removed a half-installed virtualenv synchronously from inside
the job future, blocking a runtime thread; it goes to `spawn_blocking`, with a
direct call when there is no runtime to hand it to.

The E2E list asked for a `# on table://` subscription the deploy now refuses.

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

* feat(dbt): snapshot a dynamic descriptor's graph per run

A `{{ }}` placeholder in `vars` can enable a different set of models per run, so
those runs re-ingest the graph. Keyed by version alone, each re-ingest
overwrote the last: reopening an older run showed the newer run's project, and
a model only the older run built was gone entirely — no SQL, no lineage, and
nothing the saved result could colour, since it can only tint nodes that are
there.

`dbt_node` / `dbt_edge` gain `job_id`. A run of a dynamic descriptor writes its
own snapshot under its job id and its page reads it back; a static descriptor
writes the version's graph once, under a zero-UUID sentinel, and every run of it
reads that. The sentinel is a value rather than NULL because `job_id` is part of
the primary key and Postgres does not treat two NULLs as the same key, so each
re-ingest would add a row set instead of replacing one.

`/assets/graph` takes `dbt_job_id` and prefers a snapshot when one exists,
falling back to the version's graph otherwise — so a run page passes it
unconditionally and static descriptors are unaffected. Snapshots age out after
30 days, pruned by the runs that write them, so no background sweep has to learn
about these tables.

Verified end to end: one deploy, two runs of it with `extra=yes` and `extra=no`
gating a model's `enabled`. The version's graph holds 6 models, run 1's snapshot
7 including `opt_extra`, run 2's 6 without it; the endpoint returns each run's
own and falls back to the version's when the parameter is omitted.

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

* perf(dbt): only snapshot a run whose graph differs, and prune from every run

Two costs the per-run snapshot carried, both found by measuring it rather than
by reading it.

A snapshot was written for every run of a dynamic descriptor, but marking one
dynamic is conservative: `graph_is_per_run` is true whenever `vars` holds a
`{{ }}` placeholder or `env` holds a `$var:`, which says the arguments reach dbt
and not that they change which models exist. The usual case is a date var, whose
graph is identical run after run, so the table filled with copies of an
unchanging picture — around 1 KB per model per run, which is a gigabyte or so a
month for a 200-model project on an hourly schedule. A row set now carries a
digest of its nodes, edges and relation root, and a run whose digest matches the
version's writes nothing; the read already falls back to the version's graph, so
those pages are unchanged. Only a run whose model set really differs pays.

The prune was hung off the progress reporter, which exists only for engines that
emit node events — so a Fusion or dbt-core-2x instance accumulated snapshots and
never deleted any. Retention that stops working because of an engine choice is
not retention; it runs detached from every dbt run instead.

Verified against a descriptor with a var-gated model: the version's graph holds
8 rows, a run that resolves to that same graph stores none at all, and a run
that enables the extra model stores its own 9. Both pages still render their own
project — 6 assets without the extra model, 7 with it.

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

* fix(dbt): scope every dbt_node join to the chosen snapshot

`job_id` joined the key, but only the scoping CTE and `dbt_edge` were taught to
filter on it. The outer node SELECT and the parent/child joins in the edge query
were not, so each model came back once per retained snapshot plus once for the
version's graph, and each edge matched every combination of the two — the model
count multiplied and the edge join fanned out quadratically. Measured against
one stored snapshot: 17 node rows where 8 are wanted, and 28 edge pairs where 7
are. The response dedup hid the edge blow-up from the payload, not from the
plan, and the run page refetches the graph every two seconds.

The progress table gained writers it was missing. `terminalize_running_relations`
settled only the relation-keyed table, so a cancelled or killed run — the case
that function exists for, since it leaves no `run_results.json` — showed every
in-flight model still spinning on the run page for as long as the row lived. An
agent worker cannot write the new table at all, having no database of its own,
so the read falls back to the relation-keyed one when a job has no rows there.

Also: a wrapped string literal missing its backslash put eighteen spaces in the
middle of the profile-disagreement error; a comment still described concurrent
runs of one dynamic version overwriting each other's graph, which is what
keying by job removed; and the `materialized_partition` index justified itself
by a run-page poll that has since moved to another table, though the closing
sweep still earns it.

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

* fix(dbt): give a graph snapshot a marker row, and scope what reads it

Five findings, four of which are the same mistake in different places: a
snapshot's identity was inferred from its contents.

Existence was inferred from a `dbt_node` row, so a dynamic run that disabled
every model — a legitimately empty graph — read as "no snapshot" and its page
showed the deployed models instead. The digest was a column repeated on every
node and read back with a `LIMIT 1` carrying no `job_id`, so a run could compare
itself against another run's digest and suppress a snapshot it needed. The
relation-root drift check read the same rows unscoped, so after a drift it could
find a previous run's root and conclude nothing had moved.

`dbt_graph_snapshot` holds one row per stored graph — path, version, job,
digest, timestamp. Existence is that row, the digest lives there once, the drift
check reads the deployed row explicitly, and the retention sweep deletes markers
first and then the rows no marker stands for. The digest is SHA-256 rather than
`DefaultHasher`, whose output is documented as unstable across Rust releases:
this value outlives the process that computed it, so a toolchain bump would have
silently stopped every comparison matching and quietly reinstated the duplicate
snapshots the digest exists to prevent.

`/run_progress` ignored the view token, so a share-link viewer got the graph and
was refused the progress that colours it.

A preview sent only its own three arguments, so a descriptor with a required
`{{ }}` var could not be previewed at all and an overridden one previewed a
different relation than the page was showing. The run's arguments go first now,
with the preview's three overriding.

Verified on a project whose only model is var-gated: the deploy stores a marker
with zero nodes, a run with the var set stores a marker with one, and the
endpoint answers 0 and 1 respectively rather than showing the deployed models
for both.

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

* refactor(dbt): squash the runtime's migrations into one

Ten migrations reshaping the same three tables is a history no installation
ever had. `dbt_node` gained `script_hash`, then `job_id`, then `ingested_at`,
with its primary key rebuilt twice; `graph_digest` was added by one migration
and dropped by the next after the digest moved to its own table. On a fresh
database all of that replays to arrive at a shape the schema can simply state,
and this feature has never shipped, so there is no upgrade path to preserve.

One migration now creates `dbt_node`, `dbt_edge`, `dbt_graph_snapshot`,
`dbt_run_state` and `dbt_run_progress` in their final shape, carrying forward
the rationale each of the replaced migrations recorded. The enum additions stay
in `add_dbt_lang`, since a value cannot be added and used in one transaction,
and the `materialized_partition` index stays separate because it belongs to a
table this feature did not introduce.

Verified by rebuilding: dropped the five tables, replayed from the single
migration, and confirmed the result is identical — same primary keys, the same
two composite `script` foreign keys, the same seven indexes. Every `sqlx::query!`
in the workspace then compiled against it, which checks each column's name, type
and nullability, and a deploy plus run on the rebuilt schema produced 8 nodes,
7 edges, a snapshot marker and 6 progress rows.

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

* fix(dbt): authorize snapshot reads, bound the prune, give the marker a lifecycle

`dbt_job_id` is caller-supplied and selected straight from `dbt_graph_snapshot`,
which carries no RLS — so a caller who could see the script could read any run's
model set and relation paths, which a dynamic alias or schema can encode. Both
graph queries now require the job itself to be visible, in the authed
transaction, the same gate `raw_code` already applies to the script that
produced it.

The drift check compared against the deployed graph alone, which misses the way
back: a run at root B republishes the path-keyed `asset` usages at B, and
returning the profile to A then matches the deploy and skips the refresh,
leaving those usages at B while dbt builds A. It reads the most recent ingest
for the version instead — the one that last wrote them — ordered rather than an
arbitrary `LIMIT 1`.

The prune anti-joined every non-deployed node and edge with no age predicate, so
each run scanned the whole retained sidecar and concurrent runs duplicated it.
All three deletes share one age bound again, with the sentinel spelled as a
literal so the partial indexes apply — a bound parameter cannot be proven to
match the index predicate.

`dbt_graph_snapshot` was the one dbt table nothing in the script lifecycle
deleted: no `script` foreign key and absent from both `clear_dbt_manifest*`
sites. A marker outliving its rows is read as a snapshot with no nodes, and its
digest still answers the suppression check, so an identical run would write
nothing and then render an empty graph. It cascades like the rows now and both
clears take it.

Also: the preview cleared `exclude` rather than inheriting it, since previewing
a model the run excluded reached dbt as `--select m --exclude m`; and
`terminalize_running_relations` no longer claims to cover a killed worker, which
never reaches it.

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

* fix(dbt): quote profile names, record where usages were published, stop polling the graph

A profile name comes from the project's own `dbt_project.yml` and a target from
the descriptor, and both were interpolated into `profiles.yml` as bare YAML —
including as mapping keys. A name like `prod # hidden` truncates the mapping and
a newline opens a sibling key of the author's choosing. Both are rendered as
quoted scalars now, as are the BigQuery keyfile's keys, with a test that asserts
the document still parses to exactly the keys we wrote.

The drift check read the most recent ingest, which latches: a run that returns
to the deployed root re-ingests but stores no snapshot (its digest matches the
version's), so the moved run's rows stay newest and every later run pays an
extra parse and ingest. The publisher now records the root it published the
path-keyed usages at, which is the only thing that answers "where do the current
usages point" — the deploy's own root goes stale as soon as a run republishes.

The run page polled `/assets/graph` every two seconds alongside progress, so it
re-sent every node's SQL for the length of a run — hundreds of KB a tick on a
real project, for a graph that a dynamic descriptor re-ingests exactly once
before the build. It fetches once more shortly after mount and then polls
progress alone.

Node results carry `outcome` beside `status`. `status` stays dbt's own word, but
dbt owns that vocabulary — 1.x and 2.x differ on casing and `no-op` arrived in a
minor release — so publishing only it would force a break or a lie the first
time it moves. `outcome` is the stable half a downstream script branches on.

Also: the worker's dbt entry points are `pub(crate)`, since nothing outside the
crate calls them and they resolve secrets and launch processes; and the snapshot
gate records that it is RLS-only where `/jobs/run_progress` also honours a
share-link token, which is a gap in what a shared page shows rather than in what
it protects.

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

* test(dbt): pin the graph storage invariants against a real database

Every defect review found in this area was DB-shaped — which row set a read
resolves to, which rows a clear takes, whether a snapshot exists at all — and
none of it is reachable from a unit test on a pure function. Four rounds
established these answers and nothing guarded them, which is why each round kept
finding another.

Six cases, on the harness the repo already uses for schema-shaped behaviour:
an identical run stores no snapshot and leaves no marker; a differing run keeps
its own while the version's is untouched; an empty run graph is still a snapshot
rather than an absent one; clearing one version leaves the others whole; the
path-wide clear takes the markers with it; and the sweep ages out run snapshots
while never touching a version's own graph.

`IngestedNode` gains `Default` so a test can state the two fields a case is
about rather than the eighteen it is not.

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

* perf(dbt): bound a script's stored graphs by deploy count

Run snapshots expire on a clock, but a VERSION's graph could not: its reader is
every finished run of that version, and a run page is as old as its job. So
nothing reclaimed them — a deploy graph went only when its `script` row was hard
deleted, which Windmill does not routinely do. A CI deploying on every commit
added a full model set with SQL bodies per commit, forever: roughly 200 KB a
deploy for a 200-model project, which is gigabytes a year across an instance.

Bounded by COUNT instead of age, since age is the thing that cannot be right
here. The newest 50 deploys per path keep their graph and older ones are
reclaimed, making growth `versions x models` rather than unbounded in time.
Generous on purpose: reaching the bound empties that version's run pages, so it
exists to stop unbounded growth rather than to be hit in normal use. Ordered by
the script's own `created_at`, so a late-finishing job re-ingesting an old
version cannot promote it.

Pinned by a test that deploys past the bound and asserts both halves: the count
holds, and the newest version is always among the survivors.

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

* fix(dbt): let the run page know when its snapshot has landed

The one-shot graph refetch was wrong: a dynamic descriptor's ingest happens
before the build but after cloning, dependency install and parse, so a fixed
delay either fires too early — and the run page then shows the deployed models
for the whole run, never that run's own — or keeps re-sending the whole graph
for the length of it. Neither is a timing problem to tune; the page had no way
to tell "the snapshot is not written yet" from "this run has none".

`/assets/graph` answers that directly: `dbt_snapshot_job` is the job the dbt half
resolved from, when one was asked for and found. The page polls the graph until
that is its own job, and stops. A static descriptor never snapshots, so an
attempt cap ends it there rather than polling for the run's duration.

`dbt_node.relation_root` is gone. The drift check moved to the marker's
`published_relation_root`, which left the column written on every node and read
by nothing.

`outcome` was published as the stable half of the result contract, but the
in-tree consumer still ranked and coloured from dbt's own word — so the field
existed and nothing used it. `statusRank` takes it, `DbtRunResult` passes it, and
`classifyStatus` is documented as the fallback for results that predate it and
for the live event stream, which carries dbt's word alone.

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

* docs(dbt): record what a share-link viewer actually sees

The comment at the snapshot gate said the graph "falls back to the deployed
set", which is only the rarer half of it. A share link is an extra grant for a
logged-in user who lacks access to the job, so the usual case is no read on the
script either — and then the `live` CTE matches nothing and the whole dbt half
comes back empty. A blank Models panel over working progress rows, not a
fallback.

`docs/dbt-runtime.md` now carries the analysis a follow-up needs: that relaxing
this leaks nothing, because `v2_job_completed.result` already gives that viewer
every node's `unique_id` and `relation_name` — the graph's only incremental
exposure is `raw_code`, which is gated separately on seeing the script. And the
shape of the fix: `OptViewToken` and `validate_view_token` are self-contained
enough to move into `windmill-api-auth`, which `windmill-api-assets` already
depends on, after which the gate can honour a token for that job's snapshot
alone while `raw_code` stays where it is.

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

* fix(dbt): keep model SQL behind the scripts:read scope, and unbreak CI

`/assets/graph` is authorized as `assets:read`, and RLS decides whether the
caller can see the script that produced a node — but RLS is not a scoped
token's grants. A token deliberately narrowed to `assets:read` could therefore
read model source and repository paths for scripts outside its `scripts:read`
paths. The same `build_scope_path_predicate` the macro endpoint already applies
now gates `raw_code` and `original_file_path`; the relation's shape is
unaffected, only its body is withheld.

`DbtAdapter::ALL` exists for the tests that must cover every adapter, so it is
dead in a release build and `-D warnings` failed all four backend checks on it.
It is `#[cfg(test)]` now.

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

* fix(dbt): stop the graph poll at the ingest, and type `limit` in the schema

The poll's stop condition was a snapshot appearing, with a 40-attempt cap
behind it — so a STATIC descriptor, which never snapshots, took the cap every
time and re-fetched the whole graph forty times. That is most of what removing
the poll was meant to save, and static is the common case.

The ingest runs BEFORE the build, so the first model to report progress proves
it has already happened: a snapshot absent by then is one this run never
writes. Progress arriving is now the second exit, and the cap is only a
backstop for a run that reports none at all.

`limit` is declared `Typ::Int` but `dbt_arg_schema` had no integer arm, so the
run form and the generated clients saw an untyped default and offered no
numeric control for a value the worker clamps. Covered by the schema test.

`relationOutcome` still re-derived from dbt's word while `statusRank` had moved
to `outcome`; both read it now.

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

* fix(dbt): make a retry prove its arguments still resolve the same

The saved arguments are the ones SUBMITTED, so a `$var:` in them is re-resolved
on retry. The identity did not cover the resolved values, so a variable that
changed between the failed run and the retry was accepted — and which graph the
retry then used depended on WHERE it landed: a worker holding the local
snapshot replays the saved manifest, while a database restore reparses with the
new value. Placement decided whether the resumed failures described the
relations being built.

The identity gains a digest of the resolved arguments, and is compared in two
halves because resolution happens between them. Project, warehouse, engine and
env are checkable up front; the arguments are not, because a retry request
carries only `dbt_command` and the ones to compare are the SAVED arguments after
this caller has re-resolved them. Comparing the whole string up front would have
refused every retry — which is what the obvious version of this fix does.

A row written before the digest existed has no last segment, and still restores
rather than being refused.

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

* fix(dbt): keep pre-upgrade retries working, and stop losing a late snapshot

Splitting the identity on its last `|` read a pre-upgrade row's env digest as an
arguments digest and left only `<run_identity>` as the prefix, so every saved
failure on an upgraded instance became unretryable — a regression the previous
commit's own test missed by using an identity with no `|` in it at all, which is
not what an old one looks like. The digest is tagged (`|args=`) rather than
positional, and the test now uses a real pre-upgrade identity.

The graph poll gave up after a bounded number of tries, but provisioning and
`dbt deps` precede the ingest and can outlast that on a cold worker — and the
engines that emit no node events never produce the progress that ends it early.
A finished run now reloads the graph unconditionally, and the poll's own exit
issues one last load: progress proves the ingest happened, not that the previous
tick saw it, and dbt's compile window is wider than one tick.

A `dbt retry` restores the failed run's arguments inside the worker and they are
never written back to the retry job, whose own args are just
`{"dbt_command": "retry"}` — so previewing a row on a retry's page ran without
the vars the run used. The result now carries the invocation's arguments as
SUBMITTED, so a `$var:` stays a reference and no resolved value is published.

The deploy-count sweep ran instance-wide on every dbt run: `FROM script WHERE
language = 'dbt'` has no index to stand on, and both orphan deletes are the
complement of every partial index here. It is scoped to the running script's
`(workspace_id, path)` — which `index_script_on_path_created_at` serves — and
the orphan deletes only run when a marker actually went.

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

* fix(dbt): hide dbt from module-less pickers, and stabilise the retry digests

`processLangs` feeds every language picker, including flow steps and app inline
scripts. Those are raw bodies with nowhere to carry a module bundle, and a dbt
script IS its bundle — so choosing dbt there produced a job that could only fail
once the worker looked for `dbt_project.yml`. Those two surfaces use
`processInlineLangs`, which drops the languages that need modules; a flow still
reaches dbt the way it reaches any script, by path to a deployed one.

`graph_digest` moved to SHA-256 because it is persisted and compared by a later
worker, and `DefaultHasher` is documented as unstable across Rust releases — but
the retry identity's own digests were left on it, and they are persisted in
`dbt_run_state.identity` for exactly the same comparison. A toolchain bump would
have refused every saved failure as a different project. All three go through
one `stable_digest`, length-prefixed so no split of the same bytes collides.

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

* fix(dbt): enforce the tag scope on snapshot reads, reset state between runs

A tag scope is an orthogonal hard restriction: a token limited to some tags must
not read a job outside them however else it is authorized. The snapshot lookup
went through `v2_job` RLS alone, which knows nothing about tags, so such a token
could still retrieve a run's model set and its dynamic relation paths. The same
predicate `require_job_read_access` applies for the progress half of the page is
applied here — `get_scope_tags` is already public in `windmill-api-auth`, and it
is `None` for an unscoped caller, so a normal session pays nothing.

SvelteKit reuses the run graph between run ids, and `graphTries`, `polled` and
`raw` all describe the previous job: a spent retry count stopped the next run's
snapshot poll before it began, and stale progress coloured its models with
another run's statuses. All three reset when the graph key changes.

Also a wrapped string literal missing its backslashes, which put two ~22-space
runs in the middle of the retry-refusal message.

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

* docs(dbt): put each digest helper's rationale on its own function

Inserting `stable_digest` above `split_identity` split that function's doc, so
five lines describing where the identity divides ended up introducing the
hasher. Each is back on the function it describes, stated once.

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

* fix(dbt): read the run-pinned graph through the job, not the asset graph

Pinning the asset graph to one run is job-scoped data, but `dbt_job_id` sat on
`/assets/graph`, authorized as `assets:read`. The job-read contract —
`require_job_read_access` — is five parts that pull in opposite directions (tag
scope restrictive, `created_by` permissive, app-embed restrictive-overriding,
view token permissive, RLS underneath), so plain RLS is neither a stricter nor a
looser approximation of it. Restating the parts near the graph query kept leaving
one out: first the job check entirely, then the share-link asymmetry, then the
tag scope, and the app-embed restriction was still missing and failing open.

The helper cannot be called from `windmill-api-assets`, because `windmill-api`
depends on that crate. So move the read instead of the check: the run-pinned
graph is now `GET /w/{w_id}/jobs/dbt_graph/{id}` in `windmill-api`, on the same
gate as the `run_progress` it colours, and `/assets/graph` has no `dbt_job_id`
parameter at all.

- `asset_graph_for` takes the job as an argument from an already-authorized
  caller; the route handler passes `None`.
- Extract the graph response into a `AssetGraph` component schema, now that two
  paths return it.
- The run page fetches the job route when it has a job id.

* fix(dbt): charge assets:read on the run-graph route, trust the job gate in SQL

Round 13 findings on the route moved last commit.

The scope domain comes from the URL segment, so putting the read under `/jobs`
asked a scoped token for `jobs:read` alone while returning asset-graph data that
`/assets/graph` charges `assets:read` for. A token narrowed to polling run status
could read workspace topology, and the missing-job fallback made it cheaper still
— any random UUID skipped the job gate. Both scopes are now required: the job
gate reaches this run, `assets:read` reaches asset data at all.

The `chosen` CTE re-decided job visibility under plain RLS after the caller had
already passed `require_job_read_access`. It could only disagree, and did so
silently by falling back to the deployed graph — a share-link viewer entitled to
the run was shown a different run's model set. Dropped; the contract is that a
job reaching `asset_graph_for` is already authorized.

Also: the flow editor's `+` insert menu still offered dbt (the third
`processLangs` caller, missed when the other two moved to `processInlineLangs`),
the docs still described the deleted `dbt_job_id` parameter, and the new handler
had again been inserted between `get_run_progress`'s doc comment and its
function.

* fix(dbt): resolve a pinned run's version from the job row, not script RLS

Local codex review of the branch.

A share-link viewer is entitled to the run and usually has no grant on the
project — that is what the link works around. The graph's `live` CTE resolved the
version by selecting `script` inside the viewer's RLS transaction, so it answered
for their access to the project rather than for the run they were given: the
Models panel came back blank beneath working progress rows.

A pinned run now takes its path and hash from the job row the handler already
read after authorizing the job, so `live` does not consult `script` at all.
`raw_code` keeps its own `EXISTS` against `script`, so the model bodies stay
behind access to the project. Verified under RLS as an unprivileged role: the
shape query goes 0 rows -> 1, the `raw_code` gate stays 0.

Taking the version from the job also means a caller can no longer pin one
project's version while naming another's run, since `dbt_script_hash` is ignored
when a job is given.

The run page's graph fetch is a raw `fetch`, which bypasses the interceptor that
adds `X-View-Token` to generated-client calls, so a shared page was refused
before any of this mattered; it goes through `appendViewToken` now.

Also trims three comments to the AGENTS.md limit, dropping drafting-history
rationale that belongs in docs/dbt-runtime.md.

* fix(dbt): snapshot vars-overridden runs, carry the pinned version everywhere

Second local codex pass.

A `vars` run argument overrides the descriptor's, and vars drive `enabled`,
alias, schema, database and materialization — so such a run builds relations the
deployed graph does not describe. It now snapshots under its own job id, which
per-job keying makes safe: the version's graph stays for runs that did not
override. The old comment claimed gating on it would strand the override's graph
for the next default run, which was true only when the write went to the
deployed slot.

Two sites still read the caller's `dbt_script_hash` instead of the version
resolved from the job, so `/jobs/dbt_graph/{id}` without that redundant
parameter dropped models the run's version had and a later deploy removed.

The `dbt_snapshot_job` marker re-checked `v2_job` under RLS — the recheck the
graph query itself drops. A share-link viewer got the right graph and a null
marker, so the run page refetched it 40 times before giving up.

`wmill script preview` read the bundle with the generic `__mod` suffix and
script-module parsing, so previewing a `.dbt.yaml` omitted the project and failed
on the missing `dbt_project.yml`. It uses the same suffix and verbatim read as
deploy.

* fix(dbt): key retry state by principal, not by script path alone

`dbt_run_state` held one row per (workspace, script path), and a retry replaces
the caller's arguments with the saved ones. Anyone able to run the script could
therefore retry whoever ran it last, replaying that run's literal `select` and
`vars` against the warehouse and publishing them as their own job's
`invocation_args`. Running the script was already theirs to do; seeing another
principal's arguments was not.

`permissioned_as` joins the key, so a retry resumes only state written under the
same authority. Two runs sharing an authority can already act for each other, so
this is the boundary that matches the rest of the job model.

* test(dbt): pin what a caller without access to the project sees of its run

The share-link case had no regression guard, and every fix in this area touched
one of its two halves: the graph's SHAPE has to survive a caller who cannot read
the script, and the model SQL must not.

Two cases against a real database, calling `asset_graph_for` as a member with no
grant on the project's folder: pinned to a run, the models render and `raw_code`
is withheld; unpinned, the same caller sees nothing of it, so making the first
work did not relax the second.

Both assertions were checked by mutation — reverting the `live` bypass empties
the graph, and dropping the `raw_code` script gate leaks `select 1` — so neither
passes on the code it is meant to catch.

* fix(dbt): key the worker-local retry cache by principal too

Keying `dbt_run_state` by `permissioned_as` left its worker-local twin keyed by
workspace and script path alone, so the boundary held only where the database row
was consulted. An agent worker never reads that table — `Connection::Http` leaves
`latest_job` as `None` — so there the local cache was the whole boundary and it
had none: the next principal to retry the script on that worker restored the
previous one's `select` and `vars`.

Also records the sqlx `--all-targets` trap in the update-sqlx skill: it is needed
for queries inside tests, and in a CE checkout it aborts on `tests/otel.rs`
(EE-only `otel_ee`) after having already emptied the cache.

* fix(dbt): log a dropped retry-state save, correct the run-progress contract

Saving retry state is best-effort — losing it costs a retry, not the run that
just finished — but `.ok()` dropped the reason too. The only symptom was `dbt
retry` reporting nothing to resume, which reads as a bug in retry rather than a
failed write. Found by running a real failing build against a worker whose
binary predated the `permissioned_as` column: the insert violated NOT NULL and
said nothing.

The run-progress endpoint's OpenAPI description promised an empty list for a
caller who cannot see the job. It is refused instead; an empty list means the job
recorded nothing yet or is unknown here.

* fix(dbt): return the retry-state write failure the warning was added to report

`save_run_state` discarded the insert result, so the caller's warning could never
fire and a lost retry row stayed silent — the symptom being `dbt retry` finding
nothing on another worker.

The error is held rather than returned at once: the worker-local copy is what an
agent worker resumes from, so a failed insert must not cost that too. Every exit
after it surfaces it, including the ones that give up on the local save.

* fix(dbt): decide a retry's graph from its restored args, keep local state in step

Three from the seventh local review.

A retry submits only `dbt_command`, so the vars-override check ran against an
empty argument set and left `graph_is_per_run` false. The failed run's arguments
are restored afterwards, and those are what the retry builds with — an overridden
one wrote no snapshot for its own job and its page fell back to the deployed
graph, showing the wrong enabled models, aliases and schemas. The decision is
re-asked once the restore has happened.

A failed durable write no longer publishes the worker-local generation either.
`restore` accepts a local generation only when the database row names it, so
publishing one the database never recorded made this worker reject its own newest
state and resume the previous run's — its selection and vars, or "nothing to
retry" if that one had succeeded. An agent worker attempts no durable write, so
it keeps its local copy as before.

A rename that also converts away from dbt moved the old path's retry state onto
the new one, reinstating what the conversion had just cleared and leaving one
user's arguments and results under a path no dbt script occupies. It moves only
while the destination stays dbt, and clears the source otherwise.

* fix(dbt): drop retry state when a run produced none, let module pushes fail loudly

Three from the eighth local review.

A run that never wrote `run_results.json` — cancelled, timed out, or dead before
dbt got there — left the PREVIOUS run's state authoritative in both the database
and the local pointer, so a later `dbt retry` resumed that older invocation's
failed nodes. Producing nothing resumable now clears both copies, so neither can
answer for the other.

`wmill sync push` wrapped the descriptor lookup and its deployment in one
try/catch meant for a missing parent. Any API failure or invalid descriptor was
reported as "no parent found" and swallowed, so a module-only push exited zero
with the remote project unchanged. Only the lookup is tolerated now.

Also condenses a comment that narrated how earlier status comparisons behaved.

* fix(dbt): forget retry state on pre-build exits too, drop cascade claims

A dynamic run whose pre-build `dbt parse` or graph ingest fails returns before
the save that clears stale state, so the previous run stayed authoritative in
both the database and the local pointer and `dbt retry` resumed ITS failed nodes
— writing relations the run that just failed never touched. Both exits now
invalidate, through one helper shared with the no-artifact case.

Two frontend comments described dbt producer rows as driving cascade dispatch.
The executor returns before dispatch for every dbt job and deployment rejects
`table://` subscriptions, so they promised behaviour that cannot occur; they
describe the lineage and ownership that is actually retained.

* fix(dbt): let the database decide retry state where it is reachable

A SQL worker treated "no `dbt_run_state` row" as no opinion and accepted any
worker-local generation. But no row is the authoritative answer that the last
invocation left nothing resumable, so a local pointer that outlived it — an
unlink that failed, a process killed between the delete and the removal, a stale
cache — resurrected a replaced run and let `dbt retry` write relations it never
touched. An agent worker keeps accepting its local copy: it has no authority to
consult.

Invalidation failures are logged rather than dropped, since a silent one is
exactly what leaves the pointer behind.

* docs(dbt): record how to run an agent worker locally, keep archived graphs

Every step of standing one up fails as something else: a normal build cannot
start one at all, the server's routes need a separate feature, and all three
token mistakes surface as a bare 401 on the agent with the reason only in the
server log. Written down with the error each produces.

Also keeps a dbt script's graph when it is ARCHIVED rather than deleted. The
pinned read resolves versions through a CTE that already skips archived rows, so
clearing bought nothing and emptied the Models panel of every completed run of
the project. Deletion still clears it.

* feat(dbt): let an agent worker publish its graph, through one endpoint

An agent worker was refused any dbt script whose profile comes from a Windmill
resource — the common case — because it could neither read the stored relation
root to check for drift nor re-ingest a corrected one.

Those look like two needs but collapse into one: verification exists only to
decide whether the stored graph still describes reality, so a worker that can
PUBLISH never has to ask. It stores what it just parsed.

`POST /api/agent_workers/dbt_graph/{workspace_id}` is the whole addition. It
wraps the same `replace_dbt_manifest` the SQL path calls, so digest suppression,
the marker write and retention cannot drift between the two transports, and it
refuses a job the token's tags do not cover. `IngestedManifest`/`IngestedNode`
gain Deserialize to cross the wire.

Two guards go, both now false: the pre-build refusal, and the `Connection::Sql`
gate added earlier to stop a `vars` override grounding an agent run.

Live progress stays SQL-only — that is a per-model event stream, and routing it
through the API would mean a round trip per node.

* docs(dbt): warn that a differing cargo feature set swaps the shared binary

* docs(dbt): record the verified agent-worker behaviour and the tmpfs quota trap

An agent worker now runs a dbt job end to end, retries, and publishes its graph
— confirmed with a dynamic descriptor whose per-run snapshot came back through
the new endpoint. The doc said it was refused; that was true before the endpoint
existed.

Also `WINDMILL_DIR`: on a dev box the job dies with `Disk quota exceeded (os
error 122)` writing the project's files while `df` shows free space AND free
inodes, because /tmp is a tmpfs carrying a per-USER quota. Point the worker at a
real disk rather than trying to clean up beneath it.

* chore(dbt): pin the EE revision carrying the agent graph endpoint

* fix(dbt): bind the published graph to the job, break the completed-page poll loop

Five from the thirteenth local review.

The EE endpoint took `script_path` and `script_hash` from the payload and checked
only that the supplied job carried one of the agent's tags, so an agent holding
any matching-tag job could name another script and replace its graph. Both are
read from the verified queue row now and the request carries only the job id. A
raw preview has no version, so it no-ops rather than 422ing before dbt runs.

`IngestedManifest`/`IngestedNode` take `#[serde(default)]`: they were
serialize-only, and a field the serializer skips made the whole manifest
unparseable on the receiving side.

A completed run page fetched the graph forever — `load()` assigns `raw`, which
recomputes `settled`, which re-entered the same effect. The final fetch is keyed
to the job by a plain (non-reactive) variable, and `settled` is read untracked.

The pin now names a revision that compiles: the previous one still called
`authed.tags()`, a method that does not exist, because both that fix and the JSON
response landed after it was committed.

* fix(dbt): keep a run snapshot out of the script's deployed ownership

Everything `persist_ingest` writes after the manifest is keyed by PATH — one row
set per script, describing what is deployed there. A run snapshot was still
reaching it, so a one-off `vars` override republished that invocation's relations
as the script's ownership and the workspace graph stayed on the override's
schemas and aliases: an ordinary run of a static descriptor never ingests again
to correct it, so only a redeploy would. A snapshot now stops after recording its
own rows.

The row preview also selected a bare model name, which dbt resolves across every
installed package while `show` takes a single node — a project model sharing its
name with a package's was previewed wrongly or refused. It selects the
package-qualified FQN.

* fix(dbt): forget stale retry state when preparation itself fails

`prepare_project` runs before every path that could clear it, and it fails for
reasons unrelated to the saved run — a profile that stopped resolving, a
provision cancelled, packages that will not install. The invocation still left
nothing resumable, so the previous one must not stay authoritative: a repaired
project would otherwise let `dbt retry` rebuild an older run's selection and
write relations the latest invocation never reached. A retry is exempt, since it
is trying to use that state and failing to prepare says nothing about it.

Also corrects the runtime doc, which still described agent workers as unable to
run dynamic descriptors or Windmill-resolved profiles. They publish their graph
through the API now; what they do not get is live progress and a durable retry
row, and the doc says so.

* fix(dbt): spell the whole FQN for preview, bound retained retry generations

The FQN selector added last commit was `<package>.<name>`, but a dbt FQN is the
resource's path within its package and the matcher must consume the selector and
end on equal lengths — so it matched nothing for a model under `models/marts/`,
which is the layout most projects use and the one this repo's own complex fixture
has. The middle segments come from `original_file_path`, whose first element is
the resource root the FQN excludes. Without a path it falls back to the bare
name: ambiguous across packages, but a selector dbt resolves rather than rejects.
Tested on a nested model, which is the input that separates the three spellings.

Superseded retry generations were removed only when a later run published one,
and never inside the hour-long grace period — so a burst left a manifest and a
results copy per run with nothing afterwards to collect them. At most four now
sit in the grace window, oldest evicted first.

* fix(dbt): scope preview state to the run, seed the project on a language switch

Previews are keyed by `unique_id`, which is the same string for the same model in
every run, and the run-change effect reset only the graph and progress. Opening a
second run of one project therefore showed the previous run's rows immediately,
and `runPreview` treated them as cached and refused to fetch. A generation
counter also drops a preview that resolves after navigation, which the reset
alone cannot catch.

The dbt project was seeded only by the empty-script bootstrap, but dbt is in the
ordinary language picker: reaching it by switching a draft produced a script with
no `dbt_project.yml`, which the runtime refuses to deploy or run. Both entry
points seed now, and neither touches modules that already exist.

* chore(dbt): cache the agent graph endpoint's query for the EE offline build

* fix(dbt): publish the graph a moved profile built

A run snapshot stopped before everything `persist_ingest` keys by PATH, which is
right for a one-off `vars` override and wrong for the other two reasons a run
re-ingests. `graph_is_per_run` was one bool for all of them, and the profile
drift check both sets it and reads back what the publisher recorded: a profile
moved A->B was detected by every run forever, each paying a `dbt parse` for a
snapshot nobody reads while the asset rows went on naming schema A.

The reason is carried now (`GraphRefresh`), and it decides both writes. Drift is
the version's own move, so it rewrites the VERSION's graph and republishes the
ownership that ends the drift; a dynamic descriptor snapshots under its job id
and still publishes; anything the CALLER scoped — an overridden `vars`, a
narrowed `select` — snapshots and publishes nothing, so one invocation's subset
can neither stand as what the script owns nor drop the models it left out from
the version's graph. Where they meet the caller wins, and the next ordinary run
settles the drift.

A restore also rebuilt `run_results.json` by copying the generation directory a
second time, so a burst of saves pruning it mid-restore left `dbt retry` with
nothing to resume and a job that reported success. It is written from the bytes
the restore already read; a manifest that went the same way falls back to the
parse a database restore pays anyway, and a generation that vanished before
either read falls back to the database's row for that same run instead of
reporting there is nothing to retry.

* fix(dbt): select a row preview by package, not by file path

The preview built dbt's FQN by dropping one segment of `original_file_path`,
which assumes the model root is `models/`. A project setting
`model-paths: ["src/models"]` turned `src/models/marts/orders.sql` into
`pkg.models.marts.orders`, and dbt's matcher — equal lengths, compared from the
front — resolves that to nothing: the preview came back empty for every model in
the project.

It selects `<name>,package:<pkg>` instead. The comma is dbt's intersection
operator, so this names the node by its own name and the package it belongs to,
which is what the FQN was reaching for and needs no knowledge of the resource
root. Verified on dbt-core 1.12, dbt-core 2.0.0-alpha.5 and fusion
2.0.0-preview.202, including a package shipping a model whose name the root
project also uses.

* fix(dbt): refuse a lockfile version that is not one, keep a named selector

Two things a preview reaches that a deploy does not vouch for.

A raw preview submits its own `lock`, so `engine_version` arrives from the
caller and was interpolated straight into the engine cache path — `../..` in it
made the download, extraction and rename land anywhere the worker can write,
and provisioning runs on the host rather than inside the dbt jail. Both it and
`adapter_version` (a pip requirement) are now accepted only as a plain version
token.

`effective_selector` also read any submitted `select`/`exclude` as an override
of the descriptor's named selector. The generated run form posts a default back
for every field the caller left untouched, and a selector descriptor's `select`
default is `[]` — so pressing Test, saving a schedule or firing a webhook built
the WHOLE project instead of `--selector nightly`. An override is now one that
DIFFERS from the descriptor's own value; a run that wants the whole project
despite the selector asks with `["*"]`.

* fix(dbt): let a moved profile settle, from the runs that actually happen

Two ways the drift check could never come to rest, both verified against a real
run of a real project on a normal worker.

`add_caller_args` read any submitted `select`/`exclude` as a caller's narrowing.
The generated run form posts a default back for every field left untouched, so
every run from the UI, a schedule, a webhook or a flow step carried them and was
marked caller-scoped: with the profile moved A->B, each one stored its models
under its own job id and left the workspace graph — and the root the check reads
back — at A. Since no UI run omits the field, the "an ordinary run settles it"
escape hatch was unreachable. Both this and `effective_selector` now ask one
question, `selection_is_overridden`: DIFFERENT from the descriptor's, not merely
submitted.

The root was also recorded beside the path-keyed publication rather than beside
the graph it describes, so a version that cannot claim the path — an older one
run by hash, a deploy overtaken by a newer one — rewrote its graph at the moved
root and recorded nothing. Its next run then compared against a root that was
absent or two moves stale and skipped the refresh its own run page needed. It is
written wherever the deployed row's graph is.

Verified end to end: same UI-shaped arguments before and after, the moved
profile now republishes (asset rows and version graph both move to the new
schema), a second run detects nothing and re-parses nothing, and moving the
profile back settles it again.

`prune_dbt_run_graphs` also ran from runs alone, while a deploy writes a whole
node set of its own, `raw_code` per model included: a project redeployed on
every push by CI and run nightly kept one full graph per push until the next
run, and one deployed but never run kept them for good.

* fix(dbt): drop a self-dependent effect in the run graph

`previewGen` was `$state` written by the effect that also reads it, three lines
under a `finalLoadFor` that is a plain `let` for exactly that reason. Nothing
reactive reads it — the only reads are inside `runPreview`, a plain async
function — so it becomes a plain `let` too.

* fix(dbt): pin a retry to the engine versions it resolved

`run_identity` carried the engine KIND but not the version it resolved, nor the
dbt-core 1.x adapter's. Redeploy an unchanged project after a release and it
locks a newer dbt or adapter while the saved `run_results.json` still passes the
check, so `dbt retry` feeds one version's artifacts to another — the exact
reproducibility the lockfile exists to hold. Both resolved versions are in the
identity now; a real failure and retry still resumes.

Also drops three comments that outlived what they describe: two said `[]`
clears a descriptor's selector, which `selection_is_overridden` reversed, and
one pointed at an agent-worker guard that no longer exists — the agent path
reaches the ingest deliberately and publishes through the API.

* fix(dbt): discard a run graph the page has already navigated away from

The component is reused across runs, so a slow `/jobs/dbt_graph` or progress
response could land after the reset and put the previous run's models, statuses
and failure state on the current run's page, where nothing would fetch again to
correct it. Every response is now checked against the generation it was
requested under — the counter the preview path already used, renamed for what
it means.

The graph poll also backs off. Neither of its stops is reachable for a whole
class of runs — `dbt_snapshot_job` never matches a static descriptor, and
`polled` stays empty for the engines that emit no node events — so an ordinary
run walked to the cap, re-sending every model's SQL 40 times in two minutes.

* fix(dbt): forget the previous run when the durable save fails, keep quoting

`save_run_state` returns the database error when its upsert fails, which leaves
run N-1's row and local generation in place: same project, same arguments, so a
`dbt retry` matches them and resumes an older attempt's failed nodes against
this checkout — the outcome the no-results branch twelve lines above calls
`invalidate_run_state` to prevent, reached by another door. It now goes through
the same call. Best effort, since the delete goes to the database that just
refused a write, but the local pointer is what a retry landing back here reads.

The run page also rejoined a relation's parts after `splitRelation` stripped
their quotes, so the one name the button exists to paste —
`"wh"."analytics.v2"."Order Items"` — was copied as something no client
resolves. It copies `relation_name` verbatim.

And a source on a finished run was called another project's: the check that
guards against two projects claiming one relation asks whether this run executed
the node, and a run executes no sources — they appear in no `run_results.json`.
Nothing materializes a source, so that warning could never be true of one.

Docs: the `vars`-override paragraph still said such a run does not refresh the
graph, which the table above it contradicts — it refreshes under its job id and
publishes nothing.

* fix(dbt): keep the asset rows and the version's models describing one graph

The workspace graph takes an asset's relations from the path-keyed `asset` rows
and its models, SQL, tests and lineage from the version's `dbt_node`/`dbt_edge`.
A dynamic descriptor published the former while storing the latter under its own
job id, so a placeholder that moved an alias or a schema left the current graph
with assets no model stands behind — nothing dbt contributes to them survives.
Ownership is published exactly when the VERSION's graph was written now, which
is the only state in which the two agree. Two cases are settled elsewhere by
design: an override's relations are a one-off, and a dynamic descriptor at a
moved profile keeps the deploy's ownership until a redeploy — its runs each show
their own models and it re-parses regardless, so the undetected drift costs it
nothing it was not already paying.

An agent worker has no durable row, so its local `current` pointer is the whole
of what a retry reads — and every local publication failure returned success
with the PREVIOUS run's pointer still in place. Where a row exists that is
harmless (`restore` takes a local generation only when the row names it), so the
abandonment is scoped to the agent case.

A failed `dbt show` also cleared the retry state: the preparation-failure exempts
`retry` but not a read-only command, and the run page's row preview is exactly
that, run as the principal the state is keyed by — so a preview that could not
provision took the retry away from the run being looked at.

Frontend: the run-change reset left `loading` and `failed` behind, so the gap
before the next run's answer rendered "no models in the asset graph" — a claim
about the descriptor — over a project that is fine. And three derivations argued
from "the graph is the current deploy", which the pinned endpoint made untrue;
each is still needed, for the version-graph rewrite and retention reasons now
written down.

* fix(cli): let --skip-scripts cover a script's module files

The module shortcut in `elementsToMap` maps the file and `continue`s before
every skip filter, and a module is deployed as part of its parent script — so
`wmill sync push --skip-scripts` still pushed the script whenever one of its
modules changed, and pull still overwrote them locally. Harmless while a module
was a rare helper file; every file of a dbt project is one of these now.

* docs(dbt): a dynamic descriptor's ownership stays the deploy's

* fix(dbt): read the run out of a failure whose message has braces of its own

`parseDbtRun` anchored on the FIRST `{` in the error message and parsed
everything after it. The worker appends the structured result after the error
text, and dbt's errors carry braces — a Jinja template, the compiled SQL, an
adapter's own JSON — so the failures most worth reading were the ones whose
summary and per-node outcomes the run page dropped. Every brace is tried now,
bounded, and the first that parses as a run wins.

Pins the EE revision that gives the agent publish endpoint the deleted-version
guard the SQL path takes: deletion is soft, the foreign key still accepts graph
rows, and the pinned graph query serves non-live versions, so an agent finishing
during a delete put a deleted project's model SQL back on screen. The query is
byte-identical to `persist_ingest`'s, so the offline cache already covers it —
verified with a full-EE `SQLX_OFFLINE=true` check.

* fix(dbt): seed a project when a modular draft switches to dbt

`seedDbtProject` returned whenever the draft carried any module at all, so a
modular script holding a `helper.ts` reached dbt with none of what dbt needs:
the project view is read-only, and the worker refuses a bundle without
`dbt_project.yml`, so that draft could neither run nor deploy. Keyed on the
project file now, and the seed goes in under whatever is already there — the
previous language's helpers are inert to dbt and the user's to remove.

Also records this runtime's schema in `backend/summarized_schema.txt`: the
`table` asset kind, the `dbt` script language, the five dbt tables and the
`materialization_status` enum the progress table uses.

* docs(dbt): move the pipeline-membership rationale out of the deploy path

* fix(dbt): gate a pinned run's model SQL on the version it belongs to

The `EXISTS` against `script` is the only thing standing between a share-link
viewer and the project's source, and it matched the workspace and path alone.
`extra_perms` is a grant on a ROW: archive a version that granted someone
access, recreate the path with narrower permissions, and that stale grant
satisfied the probe while the query returned the NEW version's `raw_code`. Both
probes name the hash now. The regression test drives exactly that shape and
fails without it, returning `select 2` to a caller granted only on the archived
version.

* fix(dbt): keep a delimiter an identifier escaped by doubling

Every dialect these relations come from escapes its own delimiter by doubling
it, and both split functions closed the quoted section on the first half and
reopened on the second: `"schema"."a""b"` came out as `a.b`. The manifest keeps
the real spelling, so the run wrote its per-model status and row counts under an
asset path no graph node has — the node simply never moves, which is the failure
mode this splitter exists to prevent.

Fixed in the worker and in its frontend mirror, which have to agree, with a case
per delimiter on both sides.

* fix(dbt): key retry state by the caller, not only by the principal it runs as

An `on_behalf_of` script executes every caller's job as its owner, so
`permissioned_as` names one principal for all of them and the retry state — the
durable row and the worker-local generation both — collapsed onto a single
entry. After one caller's run failed, the next could submit `dbt_command: retry`
and resume it: their arguments replayed against the warehouse, and handed back
through `invocation_args`. Nothing else separated them, and on an agent worker
the local directory is the whole boundary.

`created_by` joins the key in both places. For an ordinary script it changes
nothing — `permissioned_as` is already that caller — and a run that was itself
superseded was never resumable anyway.

Includes the offline cache for the four changed queries and the three the
pinned-graph regression test added last commit, which had none: `prepare`
without `--all-targets` does not compile test targets, so CI's
`SQLX_OFFLINE=true ... --all-targets` would have failed on them.

* fix(dbt): compare the schema too when reporting a relation that moved

`relationDrift` compared the leaf name alone, and the move it exists to report —
a profile repointed at another schema, which a later run then writes into the
version's graph — leaves every model's name exactly where it was. So the one
case that reliably produces a graph naming relations this run did not write was
the one case the notice stayed silent for.

The schema segment joins the comparison, qualified against qualified: an
unqualified one means the target's own database, which the relation names
anyway, so comparing that would report a move on every node.

* fix(dbt): bound the retry state now that it is keyed per caller

Keying by `created_by` fixed one caller resuming another's run and created a
growth problem doing it: a shared `on_behalf_of` script kept one row and one
worker directory for everyone who had ever run it, and the generation prune only
bounds files INSIDE a directory.

Three bounds, none of them new machinery. A run with nothing failed or skipped
saves nothing — `dbt retry` builds from those nodes alone, so that state could
only ever be refused — while still clearing what the previous run left, since
its failures are no longer what last happened here. The rows expire on the same
30-day clock as a run snapshot, swept per path by the prune every dbt job
already spawns. And the worker-local directories are swept there too, by the age
of the pointer a save rewrites, because their digest names neither the script
nor the caller.

* docs(dbt): the retry state is worker-affine only on an agent worker

* fix(auth): only the server may set a token label that names a user

`create_token_internal` wrote `NewToken.label` verbatim, and the auth layer reads
some labels as an IDENTITY: `username_override_from_label` maps
`ephemeral-script-end-user-<name>` to exactly `<name>`, which then becomes
`created_by` on every job that token pushes. The label is free-form request
input, so any member could mint a token that speaks as somebody else — the shape
`require_job_read_access` already works around when it refuses to trust
`username_override` and falls back to an RLS probe, and the one that made dbt's
retry-state key (`created_by`) forgeable for an `on_behalf_of` script.

The labels are refused where request input enters: the member-facing
`tokens/create`, and `impersonate`, which names its subject in
`impersonate_email` and has no business renaming the caller too. The legitimate
producers are unaffected — a job's own token comes from `create_token_for_owner`
in the worker, and native triggers and app-embed tokens build their labels
themselves rather than accepting one.

`Ephemeral lsp token` stays allowed: its override is the fixed sentinel `lsp`,
not a name the caller chose, and the editor mints exactly that label through this
endpoint for its language server. The test pins the two lists together, so an arm
added to `username_override_from_label` that lets a label choose a name fails
until it is reserved too.

* Revert "fix(auth): only the server may set a token label that names a user"

This reverts commit efaa498d82.

* fix(auth): only the server may set a token label that becomes a bare username

* revert(dbt): key retry state by the execution principal again

Reverts the per-caller key and the retention it needed. `created_by` cannot
carry an isolation boundary: it is `display_username()`, which a token LABEL
supplies, so two callers can share one value and — before the guard two commits
back — one could name a third person. GHSA-8x8x-88qc-qp4r settled that class by
refusing to trust the name for authorization, and keying on it here was the same
mistake in a new place. It also cost a migration, a PK column, two sqlx cycles
and a retention sweep to defend.

Back to `(workspace, script_path, permissioned_as)`, which is derived from the
authenticated username and cannot be chosen by a request. What that boundary is,
and the one case it does not cover, is now written down where the retry is
specified rather than left to be re-derived: anyone entitled to run the script as
that principal may resume its last failure — the same capability as re-running
that job, since running it requires the read access that already shows them the
run and its arguments — except for a run pushed `invisible_to_owner`, whose
arguments a retry still returns. Closing that means having a retry NAME the job
it resumes and authorizing it as a job read, which is a change to the run
argument, not to the key.

* fix(dbt): give a caller's own selection a graph, and load a finished run once

A run that overrides `select`/`exclude` was marked caller-scoped but not
per-run, so it ingested nothing and its page fell back to the deployed graph.
That is only right when the override NARROWS the descriptor's selection —
`["*"]`, or any model outside it, builds relations the deployed graph never had,
and those are exactly the ones whose progress, SQL and lineage had nothing to
draw. It ingests its own graph now, still caller-scoped, so the subset stays out
of what the script owns.

`relationDrift` also read a schema whose own name contains a period as a move: a
segment holding one is ambiguous — an overridden database, or a schema really
called `a.b` — so either spelling now counts as unmoved.

And a finished run fetched the whole graph twice on mount, every model's SQL
included: the reset effect and the finished-run effect both fired in the same
tick. The reset only loads while the run is in flight; the other owns the
finished case, because a snapshot can land after the run ends.

* fix(dbt): classify a restored retry by its resolved arguments

`add_caller_args` was handed `raw_args` on the retry path — the arguments as
SUBMITTED, where a `select` spelled `$res:` is still a string. `arg_list` then
refuses it as "must be a list of strings" and the retry dies before parsing,
for a reference that resolves to the very list the failed run built with. The
resolved map decides it now, matching the selection resolver and the build;
`raw_args` stays what is persisted and published, so no resolved secret outlives
the job. Verified against the shape it breaks on: a `select` from a resource,
failed, then resumed.

Two docs that outlived their code: `dbt_script_hash` is described as a fallback
for a job naming no deployed script rather than as the pinning mechanism, since
a script job's version comes from the job row and the query value is ignored
(spec plus the generated client, which carries the same sentence); and
`GraphRefresh`'s fields no longer claim a caller's selection only ever narrows,
which is the premise the previous commit disproved.

* fix(dbt): a hidden run keeps no retry state

The state is keyed by the execution principal, which every caller of an
`on_behalf_of` script shares, and a retry publishes the arguments it restored —
so for a run the other callers cannot read, that retry was the one way to see
them. The equivalence the key rests on ("resuming it is the same capability as
re-running it") holds only while the run IS readable, and exactly one run is not:
one pushed `invisible_to_owner`. Those now save nothing, and clear whatever the
previous run left, since this invocation happened. A hidden run therefore cannot
be resumed by anyone, its author included — the cheaper half of the trade, and
the reason having a retry NAME its source job is the design that would give it
back.

Verified: a visible failure still saves and resumes; a hidden one leaves no row,
and the retry after it refuses without returning the hidden `vars`.

Also collapses `GraphRefresh::caller_scoped`, which stopped distinguishing
anything once a selection override became per-run: both writers set both flags,
so `snapshot_job` and `publishes_ownership` are now one field and its negation —
which is what lets the agent payload carry a single `per_run` bit, recorded
there. `is_reserved_token_label` says why `ephemeral-` is reserved (a
client-chosen system label is a token its owner cannot list or revoke) rather
than restating a username rule its second prefix does not follow. And the
module-cache import comment states the invariant once, without the shape of the
cache that preceded it.

* fix(dbt): a pre-build failure leaves the saved run alone

Three exits cleared the retry state before `dbt build` ever ran: a preparation
failure, a failed `dbt parse`, a failed ingest. None of them touches a relation,
so the warehouse is exactly what the previous run left and its failures are still
the accurate description of it — clearing there just costs a resumable failure,
and a resume that no longer fits is refused by `run_identity` and the arguments
digest regardless. The reachable one is a cancellation during provisioning. Only
an interrupted BUILD invalidates, which the save at the end of the job already
decides.

Verified: a failure saves state, a run whose selection matches no node fails in
the ingest and leaves it, and the retry after that still resumes the original
failure.

Agent-published graphs also had no sweep — `Connection::Http` snapshots every run
and spawns none — so the EE endpoint prunes too (pinned at f58bf06), and
`prune_dbt_run_graphs` now names every writer rather than only the runs.

Drops `ephemeral-webhook-` from the reserved token labels: it yields the label
verbatim, not a bare username, so the check now matches the rule its doc and its
test state, and the reason given for it was wrong about what `is_user_token`
costs an owner. And the result contract lists `invocation_args` — the field most
needing it, being another invocation's arguments on a retry.

* fix(assets): decode a doubled delimiter when canonicalizing a table key

`split_relation` decodes it on the worker side, so the canonicalizer had to as
well: a relation whose identifier escapes its own delimiter — `"sales""east"` —
was filed by the ingest under `sales"east` and by a hand-written `table://`
annotation under `sales""east`. Two nodes for one table, no edge, which is the
exact split this key exists to prevent (decision 11). Fixed for both the name and
each half of a database-qualified schema segment, with a case per dialect.

`invalidate_run_state`'s contract also still promised the pre-build behaviour the
previous commit removed; it now names the cases that do invalidate — an
interrupted build, and a run hidden from the script's owners.

* fix(dbt): reclaim package trees no project asks for any more

The cache key covers the whole project digest — a `local:` dependency's content
is in no manifest, so nothing narrower is safe — which means every edit of a
project that declares packages publishes another full dependency tree, and
nothing ever removed one. A worker that lives through a hundred deploys held a
hundred trees until an operator cleared the entire cache by hand, and the disk
that fills fails every job on that worker, not only dbt's.

Swept by last USE, not by publication: a hit dates the tree, so a project
unchanged for months is not evicted from under the jobs still running it. The
marker is a sibling rather than a file inside the tree, which the restore copies
into the project. Staging directories go after a day — one belongs to a single
job and is removed when it ends, so an older one is from a worker that died
mid-publish. Verified by planting a 30-day-old tree and a stale staging dir: one
run reclaimed both and left a fresh tree alone.

Narrowing the key to the declared `local:` paths would fix the churn as well, and
is deliberately not done here: getting it wrong runs a project against another
revision's packages, which is worse than the disk it saves.

* docs(api): declare the dbt half of the asset-graph response

`AssetGraph` is the response of both `/assets/graph` and the new
`/jobs/dbt_graph/{id}`, and the spec described neither `assets[].dbt`,
`runnables[].dbt`, `dbt_edges` nor `dbt_snapshot_job` — so every client generated
from it saw a graph with no dbt metadata, no `ref()` lineage and no snapshot
marker, which is why the run page reaches the endpoint with a raw `fetch` and a
hand-written cast. `frontend/src/lib/gen` is gitignored, so the spec is the only
tracked description of this surface.

Declared, and the client regenerated from it now carries all four (verified: the
regen is otherwise a byte-for-byte no-op).

* fix(dbt): stop the package sweep from racing the restore it protects

Three faults in the sweep the previous commit added.

The `.last_used` marker was written AFTER the copy, and the tree it matters for
is the one at the retention edge — where the first use in a fortnight is also
when the sweep fires. So the source looked stale for the length of the restore
and a concurrent job's sweep could remove it mid-copy, failing a run that should
merely have refetched. Marked before the copy, and a restore that loses the race
anyway is treated as a cache MISS: `dbt deps` resolves the tree again, costing a
fetch rather than the run.

The sweep also sat inside `if let Connection::Sql`, though it is a walk of the
worker's own disk that needs no database — so an agent worker, which has none,
published a tree per project edit and reclaimed nothing. That is the state the
doc claimed was fixed, and the same argument the graph sweep makes two lines
above: retention that depends on the connection is not retention.

And the canonicalizer's two halves disagreed after `5890436`. `unquote_identifier`
requires the quote at both ends, so a lone `"` in an already-decoded name
survives; the schema walk treated one as opening a quote and dropped it, filing
the ingest's `sa"les` under `sales` while an annotation's `"sa""les"` decoded to
`sa"les` — the split this canonicalization exists to prevent, in the function I
had just touched. The halves share one rule now, and the test drives the decoded
spelling against the quoted one rather than asserting only the annotation's.

* fix(cli): track a dbt project's parent descriptor for every authored file

`buildTracker` decides whose top hash `wmill-lock.yaml` refreshes, and it reached
the module branch only for files matching a Windmill script extension. A dbt
project is mostly files that are not: `dbt_project.yml`, `packages.yml`, schema
YAML, seed CSVs. Editing any of them left the descriptor untracked and its
module-inclusive hash stale, so the lock disagreed with the bundle that was
pushed. Module paths are now handled ahead of that gate.

The parent was also derived by searching the RAW path for `__dbt/`, which finds
nothing on Windows, where the folder is spelled `__dbt\` — so even a model edit
was skipped there. It goes through `getScriptBasePathFromModulePath`, which
normalizes separators, and which the sibling helpers already used for this exact
reason.

The regression test drives all three shapes. With the old gate reinstated it
fails on the non-script files and on the backslash path, and passes only for
`.sql`.

* fix(dbt): re-resolve unlocked dependencies, and reclaim retry state nobody writes

A project with no checked-in `package-lock.yml` asked dbt to RESOLVE its ranges
and mutable git revisions, and dbt does that on every run. A cache hit skipped
`dbt deps` entirely and every hit refreshed the marker, so the first resolution
was pinned for as long as the project stayed in use — a version range that moved
was never picked up. An unlocked tree is now a miss once it is a day old; a
locked project keeps its tree, since the lock is in the key and pinning is what
it asked for.

Retry state: `invalidate_run_state` removed the pointer and left the generations,
and the whole state root was swept by nothing after `a236972` — so a script that
stops running, or a principal who stops running it, kept its last generations
(a `manifest.json` each) for good. Forgetting the state now prunes its
generations, and a 30-day sweep of the state root runs beside the package sweep,
for the reason that one already gives: retention that runs only for the script
being run is not retention for the ones that are not. This restores one half of
what `a236972` reverted — the local sweep, not the per-caller key or the row
retention — because it never depended on that key.

The graph orphan sweep also committed its marker delete separately from the two
deletes it gates, so an error or a restart in the gap left graph rows whose
marker was gone — and since the sweep only runs when a marker went, every later
call skipped it and those rows were unreachable for good. One transaction now.

`parseDbtRun` scans from the LAST brace: the payload is appended, so backwards
finds it immediately where forwards parsed the whole message once per brace in
the error text. And `reject_reserved_label` moved into `create_token_internal`,
the one path every caller-supplied label reaches, so the invariant sits where a
new route would break it.

* fix: undo two of my own regressions, and agree with the backend everywhere

`2e34dd48` hoisted the module check above the extension gate, which is right, but
did not bring the entry-point rule with it: a folder-layout script's METADATA is
`<base>__mod/script.yaml`, an entry-point path, and pushing it as a content file
makes the metadata pass ask for the language of `.yaml` and abort the whole
command. Reached by editing the summary of ANY modular script — pre-existing,
nothing to do with dbt. Metadata resolves to its content file now, with a test.

`d3716a09`'s unlocked-dependency refresh is reverted. It measured staleness
against the last-USE marker, which every hit rewrites, so an actively used
project never reached the threshold — and when it did fire, the re-publish
`rename` cannot land on a populated directory, so the freshly resolved tree was
discarded and the stale one re-stamped: a network resolve thrown away per idle
day. Doing it properly needs a publication timestamp and a publish that can
replace a tree; the limitation is recorded where the cache is keyed instead of
half-implemented.

`parseDbtRun` no longer counts braces in either direction. The payload is
appended pretty-printed, so its `{` is the only one at column zero — forwards
blew the cap on an error full of braces, backwards on one `{` per node, which a
few hundred nodes reaches. Test drives a 400-node failure.

And `parsePipelineAnnotations` was the third copy of the quote rule, still
splitting a doubled delimiter: the live canvas keyed `"sales""east"` as
`saleseast` where the deploy keys `sales"east`, so an annotation pointed at a
node the deployed graph does not have. All three agree now, including on a lone
delimiter in an already-decoded name.

* chore(dbt): leave cache retention and the migration's comment out of this PR

Two deliberate subtractions, so what ships is the set that has been verified
rather than the set that was written.

The package-tree and state-directory sweeps are gone (recoverable on
`dbt-cache-retention-followup`). They delete directories on a worker, they were
the newest code here, and one of them already needed a second pass for racing an
active restore — while what they buy is disk hygiene, not correctness: today an
operator's `cache_clear` reclaims these caches, exactly as it does for every
other language. Both limitations they addressed are now recorded where the cache
is keyed. `invalidate_run_state` still prunes the generations it orphans, since
that reuses the existing pruner and its grace window rather than sweeping a root
by age.

And the migration is back to its pre-PR bytes. The only change left in it was a
corrected comment, which every database that already applied this migration would
have paid for with a checksum mismatch at startup — the explanation lives in
`prepare_project` and in the design doc, which is where it is read.

* fix(dbt): pin the resolved package tree to the deployed version

A cache hit skipped `dbt deps`, so an unlocked range or mutable git revision
stayed on whatever the first worker resolved, and a retry could feed one
resolution's run_results.json to another. The deploy now records the digest of
the generated package-lock.yml, the package cache is keyed by it, and it joins
the run identity; a worker that resolves anything else is refused rather than
run.

Also carries the agent-wire round trip test for an ingested manifest, and the
EE pin for the agent publish endpoint.

* docs(dbt): state the dependency contract the deploy pins

The design doc still described the package cache as keyed by the project digest
alone, and carried an open question about refreshing unlocked dependencies that
the deploy-time pin answers: resolution happens once, at deploy, and every run of
that version installs it or is refused. Records what that costs and buys —
committing package-lock.yml makes deploys cache-hit, deploying again is what
picks up a newer range — and that these worker-local caches are reclaimed by
cache_clear, as every other language's are.

* fix(dbt): four findings from the review round

- An interrupted retry republished the results it had only restored. A retry
  starts with the previous attempt's run_results.json in place; cancelled before
  dbt rewrites it, the save dated those failures to this job, so the next retry
  rebuilt nodes this one had already redone. Verified by A/B: without the check
  the row survives, carrying the cancelled retry's id.

- The deploy refused a committed package-lock.yml that dbt itself updates, which
  it does whenever the sha1_hash it recorded for packages.yml no longer matches.
  Only a run has a resolution to be held to; the deploy establishes one.

- A second graph load for the SAME run could land out of order, leaving a
  finished run showing the deployed fallback for good.

- Directory nodes in the project tree carried no path, so two of one name at the
  same depth shared a collapse key and folded together.

* fix(dbt): name every adapter, and stop guessing which project owns a model

The invalid-adapter message listed four of the eleven accepted spellings.

And a relation may have several script producers, none of which the provenance
record identifies — prefixing the first one names a __dbt folder that does not
exist, so an ambiguous relation now shows the path inside the project alone.

* refactor(dbt): name the graph-snapshot column for when it is written

`published_relation_root` described a design persist_ingest does not use: the
root is recorded by every ingest that is not a run's own snapshot, including
one that publishes no ownership — deliberately, since a version that cannot
claim the path would otherwise record nothing and compare against a stale root
forever. The column comment said the opposite and the name followed it.

Safe to edit the migration in place because this PR introduces it: no database
outside a checkout of this branch has ever applied it. One that has needs

  UPDATE _sqlx_migrations SET checksum = decode('<sha384 of the file>','hex')
   WHERE version = 20260725084314;

alongside the column rename, or a fresh database.

* style(dbt): bring comment blocks within the four-line guidance

AGENTS.md asks for each invariant at the place someone would break it, in at
most four lines. The new dbt files carried 60 inline blocks past that, several
of them three rationales deep at one site.

Nothing durable is dropped: what covered several constraints at once is split
to the lines it constrains, and eight comments that had drifted above the wrong
test — six stacked over one profile test, two over the wrong executor test —
are reattached to the tests they describe.

* docs(dbt): carry the snapshot-column rename into the summarized schema

The rename reached the migration, both queries, the sqlx cache and the design
doc, but not the compact schema summary — which is the file agents read instead
of the migrations, so it was the one copy that could mislead silently.

* docs(dbt): bound the retry-state and package-pin residuals precisely

Two claims in the design doc were true but not precise enough to act on.

Retry state: the equivalence between resuming a failure and re-running the job
holds because folder read grants job read, which is also what grants execution.
It does NOT hold for a u/<owner> script shared through extra_perms, where no
folder policy applies and a grantee cannot read even their own on-behalf run.

The package pin: the refusal is per worker. A ranged dependency with no
committed lock reproduces its resolution only from a cache hit, so it keeps
running where the tree is warm and fails on the first cold worker.

* fix(dbt): refuse a retry of a run its caller cannot read

Retry state is keyed by execution principal, so every caller of an on_behalf_of
script shares one saved run. Under a folder that matches job visibility exactly,
but a u/<owner> script shared through extra_perms has no folder policy: a
grantee can read neither the run nor their own, while a retry published its
arguments.

The restore now applies require_job_read_access's rule — you can always read a
job you launched, otherwise the row must be visible under your own RLS.
Deliberately NOT job_perms, which carries the identity the job runs AS: for an
on_behalf_of script that is the owner, and probing with it authorizes everyone.

Verified with two members on one shared principal: bob is refused alice's run
on the u/ path and her marker never reaches his result, alice resumes her own,
and bob still resumes alice's run of the same script under a folder.

* fix(dbt): close the retry check's own gaps

The check landed with three holes and four rough edges, all found by review:

- A pruned source run was allowed. dbt_run_state outlives job retention, so
  that handed the last failure's arguments to whoever asked next. It now fails
  closed, and says so rather than claiming the run was someone else's.
- An agent worker skipped the check entirely: it reaches no database. The saved
  generation now records who launched it, and an agent authorizes the half it
  can prove — the launcher may resume. A generation written before this field
  is not resumable there.
- The row was authorized on one read and restored on another, so a run
  completing in between was restored unauthorized. restore_from_db now takes
  the job it was authorized against.

Also: restore_run_state's doc comment had drifted onto the new helper, the
not-a-member comment described the saved run rather than the caller, and the
refusal message carried a run of spaces from its line continuation.

* fix(dbt): keep the descriptor in --json sync, and fail closed on a stateless row

A dbt script's content is `<name>.dbt.yaml`, and `elementsToMap` drops every
.yaml when metadata is JSON — so a --json workspace tracked the metadata, the
lock and the whole project bundle but not the descriptor: a descriptor-only edit
pushed nothing and a fresh pull wrote a script with no source. Pinned by a test
that fails without the exemption.

And the retry check read job_id flattened, which merged "no row" with "a row
naming no job" and skipped authorization for the second. Unflattened, the row
that cannot be authorized is refused.

Also graph_digest: serde rather than {:?} for edges, and delimited parts.

* docs(dbt): state that retry authorization is by identity, not token scope

The section claimed the read-access equivalence was enforced. It is, for
identity — but the worker never sees the submitting token, and neither v2_job
nor job_perms records a scope, so a token denied jobs:read can still resume its
own principal's last failure. What it gets is the arguments as submitted, so a
reference is re-resolved under whoever retries rather than disclosed.

Also names the cases the check refuses outright, which were spread across three
commit messages and nowhere a reader would look.

* chore(dbt): repin EE after rebasing the agent-graph branch onto EE main

* fix(dbt): say why a test-only run has no models, instead of blaming the profile

A selection of tests alone builds nothing, and a test is an assertion rather
than a relation, so the ingest keeps nodes with no asset to hang on and the
graph comes back empty. The run is correct and its results render — but the
empty state named the one cause it is not, a project with no warehouse
identity, and sent the reader to their profile.

The graph itself is unchanged: retaining a selected test's attached model would
have a test-only script claim a read on every model it asserts against, which
is an asset-graph semantics change and wants its own review.

* fix(dbt): authorize a retry by RLS alone, and eight review findings

Retry authorization drops the created_by grant that mirrored
require_job_read_access's "you launched it". That name is a display name derived
from a token label, so a worker cannot tell a launcher from a collision — the
objection Codex raised on four heads. Visibility under the caller's own RLS is
the whole rule now. An on_behalf_of script under u/<owner> shared by extra_perms
is resumable by the owner alone; under a folder every caller keeps the resume.

Also, and each verified against the code first:

- The local generation is revalidated after the file work, so a newer run
  publishing state mid-restore no longer lets the superseded one resume.
- Both dbt zip lookups in sync normalize the OS separator, as the resource
  lookup beside them already did: on Windows a pulled dbt project was laid out
  as __mod, the one layout dbt cannot run from.
- A resource storing port as a string no longer silently connects to the adapter
  default; a value that is not a port is refused.
- An oversized but readable project file fails the deploy instead of shipping an
  incomplete project that compiles and fails at run time.
- cache_clear does NOT reach the dbt caches: it clears cache/ while these live
  under cache_nomount/, as bun's do. The doc said the opposite, and that claim
  was the justification for dropping the retention sweeps.
- A dbt unit test counts as a test for the empty-state message, as it already
  does for the worker's test-phase detection.
- Per-node results keep the catalog segment when rows disagree on it.
- The project panel copies through the app's clipboard helper, .dbt.yaml
  resolves back to dbt in EXTENSION_TO_LANGUAGE, and two test blocks clean up
  their temp directories.

A comment describing a race against a sweep this PR does not ship is gone.

* fix(cli): keep the oversized-file check to a bounded read

The refusal statted the file and then read it whole to tell text from binary,
which is the cost the two comments above it and isBundledModuleFile exist to
avoid: a multi-gigabyte seed would be loaded just to be refused. Same 8 KB head
read as that predicate.

* revert(dbt): accept the retry residual instead of gating it

Resuming grants no capability a caller lacks: they may already run the script as
that principal, and a plain run builds a superset of what a retry rebuilds. The
one thing a retry adds is information -- the result echoes the resumed run's
submitted arguments, for the row preview -- and in almost every shape the caller
could already read that run: a folder grants job read alongside execution, and an
ordinary script's runs are keyed under the caller's own principal. It takes a
user-path script AND extra_perms sharing AND on_behalf_of for the two to diverge.

Gating it needed an identity the worker does not have. created_by is
display_username(), which a token label supplies: trusting it authorizes a
collision, and resolving it as a username denied every labelled token -- a CI
token is `label-<name>`, no workspace member, so its own retry was refused. That
regression cost more than the exposure.

Kept, because none of it is about the caller:
- a cancelled retry no longer republishes the results it only restored,
- the restore is pinned to the row it chose rather than re-reading it,
- a newer run publishing mid-restore no longer lets the superseded generation
  resume.

The three sqlx entries the gate needed are gone with it.

* fix(dbt): select unit tests, and say why the state key is the boundary

`dbt ls` enumerated five resource types and omitted `unit_test`, which dbt treats
as its own type rather than a flavour of `test`. A descriptor selecting
`test_type:unit` or a unit test by name therefore resolved to the empty set,
which the deploy refuses outright with "matched no dbt nodes". Verified both
ways on a fresh path: the deploy fails without this and succeeds with it. All
three engines list `unit_test` among the accepted values.

The restore's state lookup now carries the reason it keys on the principal and
not the caller. That decision was in the design doc, the commit messages and a
PR thread — everywhere except the line someone would change to "fix" it, which
is where AGENTS.md asks for it and where I twice went wrong myself.

Also: the `GraphRefresh` doc claimed a dynamic descriptor publishes its graph as
the script's ownership, while `per_run_models` snapshots it under the job id and
`publishes_ownership()` returns false — only a moved profile republishes. And
`fn n` in dbt_profiles.rs had no callers left after `port_of` replaced it.

* feat(dbt): offer the resume on a failed run, where the choice is relevant

"Run again" prefills the arguments the run just used, so the obvious action after
a failure rebuilds the whole project while the cheap resume stays invisible
unless the reader knows dbt_command has a retry value.

A finished run with failed or skipped nodes now says how many, and links to its
own run form with the command already switched. A link rather than a submission:
the caller still presses Run, so nothing about permissions or arguments changes.

Deliberately not automatic. A resume is not a re-run — models are usually
rebuilt because upstream data moved, not because they failed — so a schedule
that resumed after a failure would leave every model that succeeded carrying
yesterday's data while reporting success.

* feat(dbt): put the command first, and offer the resume where the rebuild is

Three things made rebuilding the whole project the path of least resistance
after a failure, when resuming its failed and skipped nodes is what dbt offers:

- `dbt_command` was LAST in the generated signature, under `limit`. The schema's
  `order` drives the run form, so the argument that decides what the run does sat
  below the ones that only narrow it. It leads now, and the signature test
  asserts the order rather than the set, with the reason it is pinned.
- "Run again" prefills the arguments of the run it came from, so for a failed run
  it reproduces the rebuild. It now carries `dbt_retry_hint`, and the form shows
  what resuming would do instead -- without choosing it, so the caller decides.
- Its dropdown offers "dbt retry with same args" above the plain re-run, for a
  failed dbt job only: a run that succeeded has no saved failure to resume.

Verified in the browser: both dropdown labels render unclipped, the alert appears
only on that path and disappears once the command is switched, and the schema of
a redeployed script lists dbt_command first.

* feat(dbt): describe the run arguments, and hide the ones a command ignores

The form offered six fields at once with no indication that most of them apply
to one command each. `retry` reuses the arguments of the run it resumes, so
every override is ignored; `limit` belongs to `show`; `full_refresh` to `build`.

Each argument now carries what it is for, and a `showExpr` for when it applies,
which SchemaForm honours by hiding the field AND dropping its value from the
payload. Selecting `retry` leaves the command alone on the form.

Not modelled as a `oneOf`, which is the more precise shape: Windmill renders
that as a variant selector whose value is a nested object keyed by a
discriminator, so the arguments would stop being flat -- and the worker reads
them by name, as do schedules, webhooks, the CLI, saved inputs and the retry
state's own copy of them.

* fix(dbt): a retry may name the run it means to resume

The retry actions I added an hour ago appear on any failed run, but the saved
failure is keyed by script and principal rather than by job — so from an older
run's page they resumed whatever failed most recently, under a label promising
"same args". Both now pass `dbt_retry_job`, and a retry naming a run the state
no longer holds is refused with the id it does hold.

Verified: with failures A then B saved, a retry naming A is refused and names B
in the message, while a retry naming B resumes.

Also from the same review: the `select` help advertised `state:modified`, which
needs a comparison manifest this runtime supplies no `--state` for, and the
openflow note still described a dbt script as a descriptor naming a git repo
rather than the module bundle this ships.

* fix(dbt): make both retry actions actually name their run

`dbt_retry_job` is not a form field, so the banner's link to the run form dropped
it and the retry resumed whatever failed last — the bug the argument exists to
prevent, reintroduced by the affordance meant to use it. The banner submits the
run itself now, as the "Run again" dropdown item already did.

And an agent worker never applied the check at all: its `latest_job` is always
None, so the local generation was accepted unconditionally. Both reviewers found
this independently. The decision moved into `chosen_generation`, which the agent
path shares, with the refusal naming the run the worker does hold.

Pinned by a test that fails without the check, on the connection kind where it
was missing rather than on the one that already worked.

* fix(dbt): let an unchanged push be a no-op, and stop advertising a retry the form cannot aim

Two of a dbt script's fields are DERIVED by the server after the no-op check
runs: the lock, which only a dependency job can produce, and the schema, which
comes from the descriptor and which no client can derive (windmill-parser-wasm
has no dbt arm). The check compared what arrived instead of what would be
stored, so every unchanged `wmill sync push` created another version and another
dependency job -- the sync churn skip_if_noop exists to prevent. Both are now
compared as stored, with the parent required to hold a lock so a deploy whose
dependency job failed can still be retried by pushing again.

Verified by A/B on a faithful payload: without this the same unchanged push
creates a version, with it the existing hash comes back and the count holds.

The run form's retry hint is gone. `dbt_retry_job` is not a form field, so a
retry started there resumes the last failure of the script rather than the run
the message pointed at -- the two buttons name their run, that path could not.
Better to not offer it than to offer it wrong.

Also reunites a comment with the derivation it describes, 40 lines below where
an earlier edit of mine left it.

* docs(dbt): an unchanged push no longer re-resolves dependencies

The section told the reader that a byte-identical deploy re-pins a ranged
dependency, which was true only because no-op detection was broken for dbt. Now
that an unchanged push is skipped, moving a pinned resolution takes an actual
change.

* feat(dbt): offer the resume only on the run it would actually reach

* fix(dbt): a refusal names both the run asked for and the one held

* docs(dbt): say that show previews one node when several are selected

* feat(dbt): a retry names the run it resumes, and the form can fill it in

* refactor(dbt): make the run's command a oneOf carrying its own overrides

* fix(dbt): refuse a command block that names no command, and keep state off retired paths

* fix(assets): let copilot and the assets filter see warehouse tables

* fix(dbt): drop the unused db handle from the retry-principal lookup

* fix(dbt): stop a preview poll when the run page moves on

* fix(dbt): refuse an escaping packages-install-path, re-arm the graph poll on navigation

* docs(dbt): concurrent runs of one script are the script's to serialize

* fix(dbt): bound what the log tailer holds in the worker process

* fix(dbt): a stale prefill answer must not aim another script's retry

* fix(dbt): a rename leaves the old path archived, so state must not be rewritten there

* fix(dbt): keep a retry's routing inputs, and stop an app-embed token probing resumability

* docs(assets): state the authorization the graph helper expects of its callers

* fix(dbt): merge the retry into the fetched arguments, not the too-big placeholder

* fix(dbt): the command block's label decides the command, not map order

* fix(cli): an oversized dbt project file must not read as generated output

* fix(dbt): the prefill names a run only when the caller may read it

* fix(cli): refuse an oversized dbt file before anything reads its body

* fix(dbt): fold table paths as the deploy does, and preview with fetched args

* fix(dbt): stream an engine archive to disk instead of holding it in the worker

* refactor(assets): the dbt asset kind is dbt://, keyed on the relation

* chore: regenerate the auto-generated prompts for the dbt asset kind

* fix(assets): rename the kind at the callers that request it, and refuse an unknown one

* feat(dbt): show the project's models under the run form on the script page

* fix(dbt): the rename missed the dbt-edge membership keys, so the DAG lost every edge

* fix(dbt): a preview must run the dbt writer, with the form's arguments

* feat(dbt): the models graph takes the bottom of the script page, as a flow's does

* fix(dbt): an unknown outcome is not a pass, and a preview keys on its arguments

* feat(dbt): a warehouse is configured on the workspace, named like the lake

* feat(dbt): asset identity keys on the warehouse name, resolved for agents too

* feat(dbt): warehouses are configured in workspace settings

* feat(dbt): the descriptor lives in the project and is optional

* docs(dbt): the warehouse is a workspace setting and the descriptor is optional

* fix(dbt): the warehouse route names its capture, and the editor stores the map

* test(dbt): pin the warehouse setting round-trip and the job-scoped route

* fix(dbt): a project-owned profile must authorize the warehouse it names

* fix(dbt): the settings tab imports TextInput from where it lives

* fix(dbt): a warehouse name is validated wherever one enters, and an absent descriptor is not a diff

* fix(dbt): the settings inputs pass their placeholder the way TextInput takes it

* fix(dbt): pushing a project that has no descriptor is not an error

* chore(dbt): keep the module suffix private to its module

* fix(dbt): warehouses survive a fork or rename, and an empty descriptor is never a file

* fix(dbt): a workspace rename carries its dbt graph with it

* fix(dbt): a rename takes the run state with it, not only the graph

* chore(dbt): drop the query-cache entries the settings edits orphaned

* docs(dbt): state the three rules that keep an absent descriptor absent

* fix(dbt): an unreadable descriptor is an error, and an emptied one is a deletion

* feat(dbt): the warehouse is unpermissioned, like the workspace bucket

* feat(dbt): an agent worker's run reports its per-model state too

* docs(dbt): how an agent worker reports what it ran

* feat(dbt): a project-owned profile reports its database, so its models share nodes

* style(dbt): rustfmt the files this branch touched

* fix(dbt): drop the job id write_profiles no longer reads a resource with

* fix(dbt): a preview reads the graph's edges and sends the model, not the cache key

* fix(dbt): a deleted producer explains itself instead of a bare 404

* fix(dbt): one warehouse resolution path, interpolated against the job

* fix(dbt): a project with no descriptor pulls and opens in dev mode

* feat(dbt): one file tree, editable, and Test builds the model you have open

* fix(dbt): a preview asks the writer's own graph, and lfs keeps its omission

* fix(dbt): metadata stays beside the project, and only a model narrows a build

* fix(dbt): a descriptor-less project previews, and dev reloads the whole bundle

* fix(dbt): forks carry the graph, and a vanished project stops the push

* fix(dbt): no-auth resolves warehouses, and a preview runs the version it read

* fix(dbt): a retry recognizes its own run when the profile carries a job token

* fix(dbt): the project marker is not deletable and the descriptor path is reserved

* fix(dbt): a templated adapter type is refused, and a removed project archives

* perf(dbt): the manifest lands in batches, and .py models are creatable

* fix(dbt): only a 404 excuses a failed archive, and edges are covered

* fix(dbt): a python model narrows the build, and a failed removal is not silence

* fix(dbt): a settings name may be any word, and the reserved path is stated once

* fix(dbt): an absent descriptor needs its project, and the form matches the run

* fix(dbt): a bundle is dbt by language, and both job routes agree who may call

* docs(dbt): every identity doc names the warehouse, as the code does

* perf(dbt): an agent posts its run's progress once, and show cannot touch a seed

* fix(dbt): bound what a warehouse may be named, and say who may ask

* fix(dbt): a run's graph withholds what the project's author wrote, and dbt_project.yml is rendered before it is read

* feat(dbt): withhold dbt from the language picker until it has its own editor

* fix(dbt): refuse a path claimed by both a dbt project and an ordinary script

* test(dbt): the bundle assertion is a set, not an order

* fix(dbt): a project's generated dirs are re-read when its config changes

* test(dbt): assert the synthesized descriptor without assuming a separator

* fix(dbt): both push paths refuse a shared path, and every writer sweeps its progress rows

* fix(dbt): preview only what dbt show can select, and a retry keeps its command block

* fix(dbt): an identifier carrying a path separator gets no asset key

* fix(dbt): no engine ships in an image, and show may name only one node

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:58:05 +00:00
3b95a2d096 feat: reusable AI agent steps with rigid linking and edit/fork (#9825)
* feat: reusable AI agent steps with hybrid linking and evals

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

* feat: make linked AI agents rigid (read-only) with unlink-to-fork

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

* feat: show inherited agent config read-only on linked step

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

* feat: edit/update a saved agent in place via upsert

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

* feat: bind linked AI agent tool inputs to host flow context

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

* feat: rebind linked AI agent tool inputs via graph tool nodes

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

* fix: linked AI agent tool nodes, step test, and read-only card

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

* chore: remove ai_agent resource type migration, sync from hub instead

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

* chore: remove AI agent eval suite and run endpoint, defer to later

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

* chore: unwire eval routes, types and UI (completes eval removal)

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

* docs: update reusable AI agents guide for eval removal and tool rebinding

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

* chore: regenerate system prompts for AIAgent agent/tool_inputs schema

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

* fix: strip brain transforms on link, avoid dirtying flow on tool open

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

* fix: flow-local test form and linked-agent marker in read-only graph

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

* fix: store linked tool overrides as diff from resource base

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

* feat: resolve linked agent tools in read-only viewer with fallback

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

* fix: use operating workspace, block non-static provider, warn on unbound tool inputs

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

* fix: resolve linked parent's tools from resource for nested agent tool lookup

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

* fix: scope linked-agent tools by flow path, thread workspace to path check and embedded viewer

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

* fix: strip flow-context tool inputs on agent save, drop unbound-inputs warning

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

* fix: persist agent edit mode across tool selection, show linked tool code read-only

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

* feat: show linked agent resource path in node definition panel

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

* refactor: edit linked tool inputs in step panel, make tool nodes display-only

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

* refactor: wire step-panel tool bindings (completes display-only pivot)

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

* fix: single scroll for linked card, agent path as node label, drop fill-inputs in tool cards

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

* style: align linked-agent UI with design tokens and components

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

* fix: separate linked tool select target from module id to unbreak agent clicks

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

* fix: save agent tool inputs verbatim, host flows override via tool_inputs

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

* fix: scope agent edit state by flow path, require linked-tools scope at init

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

* fix: block saving an agent whose static provider is incomplete

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

* fix: type errors in agent tool bindings and save drawer input

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

* fix: key agent edit state by workspace, resync tool bindings on external changes

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

* fix: include workspace in linked-tools scope and tool schema fingerprint

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

* fix: remove unused workspace prop from FlowModuleSchemaMap

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

* refactor: drop linked-agent placeholder tool node, path label suffices

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

* fix: workspace-qualified resource links, guard stale tool schema loads

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

* fix: keep flow tool overrides out of the agent on edit, fold only on unlink

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

* fix: fold preserved tool overrides into the step on edit cancel

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

* fix: refuse overwriting non-agent resources on save, show memory kind on linked card

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

* fix: consume picker value, invalidate edit state on undo/reinit, cap nested agent tools

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

* fix: guard in-flight edit fork against restores, migrate edit state on rename

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

* refactor: validate agent edit state by fork identity instead of path keys

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

* fix: key agent edit entries by fork marker alone, immune to editor nesting

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

* fix: keep agent edit state across structural graph edits and flow renames

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6kq9PYqNdc5q7ubidBYAs

* fix: centralize agent edit reanchor, guard in-flight saves, seed rename scope from flow path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6kq9PYqNdc5q7ubidBYAs

* fix: ancestry-keyed edit reanchor and doc-scope sweep for republished linked tools

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6kq9PYqNdc5q7ubidBYAs

* fix: guard stale linked-tool fetches and resolve while-loop nested linked agents

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6kq9PYqNdc5q7ubidBYAs

* fix: drop empty tool override entries on revert and correct stale viewer comment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6kq9PYqNdc5q7ubidBYAs

* docs: drop stale eval mention from the linked-agent comment

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

* fix: deploy linked agent resource, guard viewer fetches, align tools schema

Address review findings on the reusable-agent branch:

- Cross-workspace deploy never collected a linked step's `agent` resource, so
  the deployed flow failed at runtime unless the agent already existed there.
- The read-only viewer published resolved tools without the generation guard
  flowState uses, letting a superseded link's tools win a race. Share one
  guarded publisher (`publishLinkedAgentTools`) between both call sites.
- `tools` was still required in the OpenFlow AiAgent schema while the
  deserializer defaults it, rejecting hand-authored linked steps; make it
  optional and narrow the call sites.
- Overlay `tool_inputs` in the non-linked branch too, so a flow persisted
  while a step sits in "Editing" mode still binds tools to this flow.
- Cap the linked-tools store's scope map; nothing evicted it before.
- Drop the orphaned `.sqlx` entry left by the eval removal, regenerate the
  copilot OpenFlow schema, and fix the generator's nested-`z.record` arity.
- Move `refreshFlowStateStore` out of `agentEditStore` into its own module.
- Document that linked agents' tool scripts are outside the lock pipeline.

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

* chore: regenerate system prompts for optional AIAgent tools

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

* fix: follow saved-agent deps on deploy, accept the linked shape in the schema

Round-18 review findings:

- Deploying a linked flow queued only the outer ai_agent resource. Follow
  `$res:` refs inside a resource value (every UI-saved agent has a provider
  resource) and the agent's own tools, which reference scripts, flows, MCP
  resources and nested linked agents by bare path.
- The AiAgent input_transforms schema still required provider/output_type,
  so it rejected the very shape linking persists (brain transforms stripped,
  flow-local inputs kept). Only user_message is always present.
- dfs traversed `value.tools` unconditionally through a cast, which throws on
  a linked module that omits it now that the field is optional.
- Trim the flow-refresh invariant comment to the 4-line limit.

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

* fix: recurse into inline nested agent tools on deploy, require provider when unlinked

Round-19 review findings:

- The deploy walk only inspected a saved agent's top-level tools, so an inline
  nested agent tool's own scripts, flows and MCP resources were skipped.
  Recurse into it; a linked one is still queued as a resource instead.
- Normalize a `$res:`-prefixed MCP tool resource_path like other refs.
- Dropping provider/output_type from the schema's required list also let a
  standalone providerless agent validate, which deploys clean and then fails
  on every run. The constraint can't go in the schema: an `anyOf` makes
  AiAgent a union, which breaks the FlowModuleValue discriminated union it
  belongs to (verified: zod throws "Invalid discriminated union option").
  Enforce it in validateFlowModules instead, next to the other cross-module
  checks, via a shared collectProviderlessAgentIds.
- Correct the deploy paragraph in the docs: provider resources are traversed
  now.

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

* fix: follow linked tool_inputs overrides on deploy, untrack vitest artifact

Round-20 review findings:

- A linked step's `tool_inputs` override replaces the resource tool's default
  at runtime, so a static `$res:`/`$var:` override is the dependency the flow
  actually uses. The deploy walk queued only the saved agent, leaving runs in
  an empty target workspace to fail on the missing override target. It also
  never scanned an aiagent module's own input_transforms, since the scan was
  gated to script/rawscript/flow.
- Extract the pure walkers to deployDependencies.ts and cover them: three
  rounds have each found a further gap in this one function.
- Untrack a vitest cache artifact committed by accident, and ignore a
  repo-root node_modules/ (only per-package paths were listed).

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

* fix: collect inline agent provider and tool deps, correct tool_inputs docs

Round-21 review findings:

- An inline agent's provider credential sits inside an object-valued static
  transform, so the top-level string check missed it and such a flow deployed
  without its provider. Walk transform values instead of string-matching them.
- An inline agent's own tools were only partly reachable: getAllModules drops
  MCP and websearch tools, so their resources were never queued. A standalone
  agent module now recurses through agentResourceDependencies, and the module's
  own input_transforms are scanned inside aiAgentModuleDependencies so one
  function owns the whole step rather than splitting it with the caller.
- `tool_inputs` was documented as empty/absent for non-linked steps, which
  contradicts the runtime applying it when `agent` is unset so a flow persisted
  mid-Edit keeps its bindings. Describe that case in both the Rust doc and the
  OpenFlow description.

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

* fix: keep linked steps brain-free on load, gate stale agent fetches, log linked tools

Round-22 review findings:

- loadSchemaFromModule filled every AI agent schema key with a placeholder
  transform, re-adding provider/memory to a linked step that deliberately
  carries none — persisted on the next save and rejected by the generated
  Copilot schema. Fill only the flow-local keys when the step is linked.
- The linked-resource fetch was neither aborted nor tagged, so switching a
  step from agent A to B could publish A's tools under B and show A's brain
  next to B's link. Tag each result with the (workspace, path) it was fetched
  for and drop the ones that no longer match.
- "Test this step" passed no tools for a linked agent, and the log viewer
  drops tool_call entries it cannot resolve to a definition, so the agent's
  invocations vanished from the log. Pass the resolved resource tools.
- Correct the cancel-edit comment: the runtime does apply tool_inputs on an
  unlinked step, and folding is what leaves nothing for it to overlay.

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

* fix: pin the edit session across saves, resolve linked tools in the run viewer

Round-23 review findings:

- Cancel stays enabled while a save awaits its requests, and it keeps the
  `tools` array identity, so the old guard passed and the completing save
  relinked the step and cleared the edits Cancel had just kept. It also
  accepted any replacement edit marker. Pin the path being saved and require
  the marker to still hold it, which still tolerates a content-preserving
  refresh re-anchoring the marker onto a clone.
- Resolve linked agents' tools in the run/status viewer too: it reads
  module.value.tools straight from raw_flow, which is empty for a linked step,
  so AIAgentLogViewer dropped every tool_call it could not match and the graph
  drew the agent with no tool nodes. Same gap the previous commit closed for
  "Test this step" only.
- Drop the overlay call-site comment: it claimed resource defaults are
  discarded and unmatched keys ignored, while overlay_tool_inputs preserves
  defaults and inserts new keys, as its own test asserts.

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

* fix: scope linked tools without the trigger-node path, keep the standalone save guard

Round-24 review findings, both regressions from the previous commit:

- Passing `path` to the run viewer's graph also switched on its Trigger node
  (`triggerNode ? path : undefined`), which reads a TriggerContext that
  /run/[...run] does not provide — the page threw "Cannot read properties of
  undefined (reading 'triggersCount')". Give the graph a separate
  `linkedToolsPath` for the tools bucket so the two stay independent.
- The rewritten save guard tracked only the edit path, so a plain "Save as
  agent" no longer noticed the step being replaced mid-request (undo, session
  sync): the replacement has no edit path either, so the stale completion
  relinked it and stripped its brain. Keep the array-identity check when there
  is no edit session, and use path re-anchoring only when there is one.

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

* fix: keep recorded tool calls in run history, send tool_inputs from step previews

Round-25 review findings:

- The agent log viewer dropped any recorded tool_call whose definition it
  could not find among the supplied tools, so renaming or removing a tool —
  or losing read access to a linked agent's resource — erased calls that had
  actually run. Render the recorded call labelled by its function name; its
  args, logs and result come from the child job, not the definition.
- "Test this step" sent tool_inputs only for a linked step, but a step forked
  for editing has no `agent` while still carrying the flow's bindings, which
  the runtime overlays. The preview ran resource-authored defaults instead of
  the bindings under test. Send them from both branches.
- Polling a running flow replaces `job` every tick, so the run viewer re-read
  every linked agent's resource each time. Key the fetch on the set of linked
  steps instead.

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

* fix: never discard edits made during a save, isolate the run viewer tools bucket

Round-26 review findings:

- The agent editor stays live while a save is in flight, so edits made after
  the snapshot were not in the resource yet linking stripped them from the
  step too, losing them outright. Compare the config against the snapshot on
  completion and, if it moved, leave the step alone and tell the user to save
  again.
- The run viewer published into the editor's `${ws}:${flow path}` bucket, so
  opening an older run in the preview pane could flip the edited flow's tool
  nodes to that run's agent. Key it by job instead.
- Drop the now-unreachable undefined filter in the agent log viewer.

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

* fix: claim the linked-tools generation on direct publishes and clears

Round-27 review findings:

- The step editor wrote resolved tools (and cleared them on unlink) straight
  into the store, leaving the fetch generation untouched. An older in-flight
  load for the previous agent then still passed its own check and overwrote
  them, so the graph and binding editor could show agent A while the step
  links to B. Claim the generation before those writes.
- Correct two comments that still described unmatched tool calls as dropped;
  they are kept and labelled by their recorded name.

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

* fix: retain the loaded linked agent, rebuild run logs when tools resolve

Round-28 review findings:

- Rejecting a superseded resource response left the card with nothing: a late
  reply for a previous agent replaces `linkedResource.current` and no refetch
  follows, so the linked step lost its brain, tools and provider warning until
  remount. Retain the last response that matched the current link instead.
- The agent log viewer built its module list on mount only, so a linked
  agent's asynchronously resolved tools never replaced the placeholders, and
  switching between completed runs reused the first snapshot. Rebuild on a
  value key — callers rebuild the agentJob object each render, so tracking its
  identity would reload in a loop.
- Refresh a linked-tools scope's recency when it is read, not only when it is
  published: a run viewer opens one bucket per nested job, which could
  otherwise evict the bucket a still-displayed run is using.

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

* fix: supersede stale log reloads and stale tools on a link change

Round-29 review findings, both on the reloads added last round:

- Every prop change starts another loadToolCalls, and it awaits child-job
  requests before writing the shared view, so a slower reload for a previous
  run could restore its logs and tool states over the run now selected — or
  replace newly resolved definitions with an earlier empty-tools snapshot.
  Build the states locally and let only the newest load publish, including the
  parent's index-keyed job cache.
- While a newly linked agent resolves, the previous agent's tools stayed in
  the store, so its bindings were editable against a step already linked
  elsewhere, and a failed load left them indefinitely. Clear them once the
  link moves away from what this component published; tools resolved at flow
  load are untouched, so selecting a step still doesn't flicker.

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

* fix: resolve a run's linked agents in the run's own workspace

Round-30 review finding: the run viewer fetched linked agent resources with
the navigation workspace, but session and fork previews render it with
`workspaceId` pointing elsewhere. Those runs resolved nothing — or an
unrelated resource sharing the path — losing tool nodes and log definitions.
Prefer the explicit override, then the job's own workspace. The store scope
stays keyed on `workspace` so it still matches what FlowGraphV2 reads; the
job id in the key already makes the bucket unique.

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

* fix: refetch a run viewer's linked tools if its scope is evicted

Round-31 review nit: the viewer publishes one scope per mounted nested job,
hidden ones included, so a loop with many loaded iterations can push a
displayed scope past the store's cap. Nothing refetched it afterwards — the
set of linked steps had not changed — leaving the run without tool nodes or
log definitions. Track the store and republish when the bucket is gone;
publishing always writes a key, so this settles instead of looping.

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

* fix: retain in-use linked-tool scopes instead of refetching evicted ones

Round-32 review findings. Republishing an evicted scope settles for one
scope but not against the cap: with more than 32 mounted nested jobs holding
linked agents, restoring one necessarily evicts another, and that mutation
reran every viewer's effect — an endless round of resource requests.

Hold a scope for as long as a viewer is mounted and skip retained scopes when
evicting, so buckets in use are never dropped and nothing has to refetch. The
cap yields to correctness when everything mounted is in use.

Dropping the publish key also restores refetching when the fetch workspace
changes for an otherwise unchanged job and link.

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

* fix: guard non-static brain edits during save, retain every displayed scope

Round-33 review findings:

- The in-flight edit guard compared the saved config, which holds only static
  brain values. A computed system prompt, memory or temperature changed while
  the save was awaiting the API therefore compared equal, and linking stripped
  it with no warning. Compare what linking actually discards — every brain
  transform and the tools — leaving the flow-local inputs free to change.
- Retaining run-viewer scopes made them fill the cap, and eviction then picked
  any unretained scope, including the editor bucket a user is looking at, with
  nothing to refetch it. Retain the scope each graph draws from for as long as
  it is mounted, so every displayed bucket is protected.
- A failed agent job has no parseable action list; the loader returned early
  and left the previously selected step's tool tree under the new header.
  Clear the view instead.

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

* fix: resolve only flow modules in viewer scans, prune scopes on release

Round-34 review findings:

- Both viewer scans used the default dfs, which descends into agent tools, and
  published each linked agent under its bare id. Tool ids imported from a
  resource are not flow-global, so a nested linked agent sharing an id with a
  top-level step superseded that step's fetch and showed its tools instead.
  Scan flow modules only — the graph resolves the store per module node.
- Scopes skipped while retained were never reconsidered, so closing views left
  the store over its cap for the tab's life. Prune on release too.
- Correct two comments that still argued the premises the retain mechanism and
  the read-recency policy replaced.

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

* fix: don't report success when a save left the step unlinked

Round-35 review nits:

- persist warns that changes made during the save are not in the resource and
  leaves the step alone, but both callers then toasted success unconditionally,
  burying the only actionable message. Report whether the step was linked.
- Condense the tool_inputs invariant to the four-line limit.

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

* fix: seed the published link at mount, keep run history for toolless agents

Round-36 review findings:

- `publishedFor` started unset, but initFlowState has already published for the
  step's link by then. A link change landing before this component's own
  request therefore skipped the clear, leaving the previous agent's tools under
  the new link — indefinitely if the new one fails. Seed it from the link at
  mount.
- A standalone agent that omits `tools` kept `undefined` here, and the gate
  downstream then hid the AI message and tool-call history behind the generic
  result view. Default to an empty list like the other consumers.
- A save that lands after the step was replaced writes the resource but leaves
  the step alone; say so instead of closing the drawer with no outcome.

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

* fix: qualify nested agent tool store keys, keep an empty tools identity stable

Round-37 review findings:

- The step editor keyed the linked-tools store by the bare module id for
  nested agent tools too. Those ids come from a resource and are not
  flow-global, so a nested linked agent sharing an id with a top-level step
  read that step's tools — then overwrote them once its own fetch landed.
  Qualify the key by the parent agent, as the edit store already does; flow
  modules keep the bare id the graph looks up.
- The `tools` binding handed the editor a fresh [] on every read when the
  module omits the field — a shape this PR made valid — so the save guard's
  identity check never matched and such a step could never link. Read through
  one shared empty array instead.

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

* fix: accept the first tool on an agent module that omits tools

Round-38 review nit: the graph's tool insert required an existing `tools`
array, so a module authored without the field — valid since `tools` became
optional — swallowed the insert while still pushing history and dispatching a
change. Create the array on first use.

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

* fix: don't evict a scope on the write that created it, and cover the store

Round-39 review findings:

- A rename removed the retained old key from the order but the new one is not
  retained until readers re-run, so eviction deleted the fresh bucket
  immediately. Reorder without evicting; the next publish or release enforces
  the cap, by which point the new key is held.
- Writing the test for that surfaced the same shape in touchScope: it evicts
  right after appending, so once every older scope is retained the scope just
  published was the only eligible victim and was dropped at once. Exclude the
  scope being written.

Add the store's first test: retention, eviction past the cap, pruning on
release, and the rename handoff — four rounds landed fixes here with nothing
pinning the behaviour.

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

* fix: re-resolve linked agents when a wholesale edit changes the links

Round-40 review findings:

- Undo/redo, YAML apply, AI apply and session restore swap a step's `agent`
  without re-running initFlowState, and the step editor only watches the step
  it is mounted on — so an unselected step kept showing, and binding against,
  the previous agent's tools. Re-resolve from the editor whenever the set of
  links changes.
- Document that linked resolution is live rather than pinned: an edit landing
  mid-run affects steps that have not started, and a nested agent tool looks
  its definition up by id when its own job starts, so it can run a changed
  definition. Pinning would mean carrying the resolved definition into the
  child job instead of its id; inline agents are unaffected because their
  tools are snapshotted with the flow value.

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

* fix: per-module empty tools identity, invalidate tools when a link is replaced

Both findings are over-corrections in the two preceding commits:

- The shared empty-tools array made identity stable, but stable everywhere: a
  wholesale edit that keeps the module id reuses the component, so when both
  the old and the replacement module omit tools the save guard saw no change
  and could link and clear the replacement. Hand out one empty array per
  module value, which a replacement always renews.
- The editor's link watcher resolved the replacement agent without dropping
  the previous one's tools first, so a step selected before the fetch landed
  still showed agent A under link B — and the freshly mounted editor seeds
  itself from B, so it could not tell. Clear the entry when the link for a
  module changes, seeding the map from the graph so the first run doesn't
  refetch what initFlowState just resolved.

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

* fix: reserve graph space for linked tools, re-resolve only changed links

Round-41 review nits:

- The layout reservation read the module's own `tools`, which is empty for a
  linked agent, so its display-only tool nodes were drawn over the node above
  in read-only viewers. Count the resolved tools for a linked step.
- The editor's link watcher refetched every linked agent on each run. Resolve
  only modules whose link actually changed, and skip the pass entirely on a
  rename, where the scope sweep has already carried the buckets over.

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

* fix: protect a renamed scope until it is retained, drop the phantom tool row

Round-42 review nits:

- Readers release the old scope before retaining the new one, so a migrated
  bucket is unretained in between and, over the cap with everything else held,
  was the only thing eviction could take. Protect a just-migrated scope until a
  reader retains it, and cover that release/retain order in the store test.
- The layout reserved an add-tool row for linked agents, which have no add-tool
  node, leaving dead vertical space. Match computeAIToolNodes.
- Re-resolving links no longer short-circuits on a rename: comparing each
  module still costs nothing when only the path changed, and a restore that
  renames and relinks in one tick now gets both.
- Hoist the duplicated linked-tools lookup in the graph's store update.

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

* fix: kill a scope's in-flight fetches before migrating it

Round-43 review finding: fetch generations are keyed by (scope, module), so a
resolution still running against the pre-rename scope keeps a valid generation
there. It publishes into the old bucket after the rename, and the doc-scope
sweep — which gives the source precedence — carries it forward over a link
resolved since under the new scope, leaving the graph and binding editor on the
previous agent's tool ids with nothing to refetch them.

Invalidate the source scope's fetches before each migration, and pin the
behaviour: the new test fails without the invalidation.

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

* fix: re-resolve links a scope sweep cancelled, and only sweep a real bucket

Round-44 review findings, both on the previous commit:

- Invalidating the source scope killed fetches that were perfectly current —
  a link still loading when the rename landed — and nothing restarted them,
  because the watcher already records that link. Resolve again, in the
  destination, every link the migration left without tools.
- The doc-scope sweep ran on every store version bump, so during a draft
  refresh the first completed fetch cancelled the others mid-flight. Skip the
  sweep entirely when the source scope holds nothing.
- Condense a six-line invariant to the four-line limit.

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

* fix: split rename from doc sweep, hide brain fields of nested linked agents

Round-45 review findings:

- Two reviewers disagreed about invalidating a scope whose bucket is empty,
  because the two callers differ. A rename is a cut-off: every fetch still
  running against the old scope is stale whether or not anything resolved
  there, so it always invalidates. The doc-scope sweep has no cut-off — those
  fetches belong to the refresh in progress — so it still waits until that
  scope holds something.
- Recording the swept links as published undid the rename+relink fix: a
  restore that renames and swaps a link in one tick would keep the previous
  agent's tools with nothing to refetch them. Leave that comparison to the
  watcher, which compares links rather than presence.
- A nested agent that is itself linked was offered the whole agent schema in
  the tool bindings, but the runtime overlays only its flow-local inputs, so
  the rest were collected and dropped. Show what actually applies.
- Condense the hybrid-linking comment to the constraint.

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

* fix: don't resolve a shared agent's tool defaults when loading it

Round-46 review finding: the whole agent resource was interpolated before
tool_inputs was overlaid, so each tool's default `$res:`/`$var:` resolved
first. A host flow overriding a default that points at the author's resource
still had to resolve that resource, and an unused tool whose default is
unreadable in the consumer's permission context failed the agent outright —
defeating the point of sharing an agent across contexts.

Read the resource raw, overlay the host's overrides, and interpolate only the
brain; each tool resolves its effective inputs when it executes. The nested
tool lookup reads raw too, since it only needs definitions.

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

* fix: interpolate the brain before overlaying caller inputs

Round-47 review findings, all on the previous commit:

- user_message and user_attachments were inserted before interpolation, so
  they went through it a second time: a user message of `$WM_TOKEN` expanded
  to the job token and was sent to the model provider. Interpolate the
  resource first, then overlay the already-resolved flow-local inputs.
- The relink watcher skips tool nodes, so a linked agent nested as a tool kept
  the previous agent's entry through undo, YAML/AI apply or a session restore,
  and the step editor seeds itself from the new link and cannot tell. Emit the
  ancestry-qualified key for those too.
- Correct the guide, which still named the interpolation path this branch
  replaced, and condense two invariants to the four-line limit.

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

* fix: deploy $jsonvar deps, key run logs by tool identity, seed only top links

Round-48 review findings:

- The deploy walkers recognised `$res:` and `$var:` but not `$jsonvar:`, which
  the worker resolves too, so a secret referenced that way by an agent brain,
  a saved tool default or a host override never reached the target workspace.
- The run log rebuilt only when a tool's name or the tool count changed, so a
  refreshed resource that altered a tool's path, code or id behind the same
  name kept showing the old definition. Key on the array identity instead: the
  store swaps it exactly when the contents differ.
- Nested linked agents were seeded as already published, but initFlowState
  resolves only top-level links, so their tools never loaded until their
  editor was opened. Seed what initFlowState actually publishes.

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

* fix: let the watcher's fetch survive the step editor's stale-clear

Round-49 review nits:

- On a relink the step editor claimed the fetch generation before clearing the
  previous agent's tools, which discarded the watcher's already-running fetch
  for the new link. The tool nodes then only appeared if the step stayed
  selected until the editor's own refetch landed. Clear without claiming: the
  watcher superseded the old fetch when the link changed, so nothing stale can
  return. Unlink still claims, since no watcher fetch covers it.
- Condense the store's opening invariant to the four-line limit.

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

* docs: condense the stale-clear invariant

Round-50 review nit. Also records why the branch deliberately doesn't claim a
fetch generation: a reviewer asked for the opposite this round, but writing
`agent` re-runs the editor's watcher, which supersedes the old fetch and
starts one for the new link — claiming here would discard it.

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

* fix: guard Edit/Unlink by step identity, not just the link path

Round-51 review finding: forkFromResource compared only the agent path after
its fetch, so a module replaced mid-request while keeping the same link passed
the check — the stale continuation then wrote the fetched brain and tools into
the replacement and unlinked it. Compare the step's own `tools` array too,
which is one instance per module value and so identifies the step.

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

* fix: report an Edit or Unlink abandoned because the step changed

Round-52 non-blocking note: forkFromResource returns undefined when the step
was replaced mid-request, and both callers treated that as do-nothing, so the
click looked ignored. Say what happened, as the save path already does.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: hugocasa <hugo@casademont.ch>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@aanthropic.com>
2026-07-28 01:38:16 +02:00
GuilhemandClaude Fable 5 0f62891d43 fix(ai): stop teaching nonexistent while-loop iter.value state-carrying (#10345)
* fix(ai): stop teaching nonexistent while-loop iter.value state-carrying

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

* fix(ai): scope while-loop results guidance to cross-iteration reads only

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

* fix(ai): drop unverified wmill state-helper fallback from while-loop guidance

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

* fix(ai): document supported cross-iteration results state in while loops

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

* fix(ai): rescope while-loop fast-path rule and add results-carrying example

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 10:42:33 +02:00
hugocasaandClaude Opus 4.8 7ebfad382a feat(ai-agent): give tools a real description instead of the tool name (#10083)
* feat(ai-agent): use a real tool description instead of the tool name

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

* fix(ai-agent): render tool-name error full width and hoist it above the description

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

* fix(ai-agent): make tool description field hug its content so a single line is vertically centered

Add an optional minHeight param to the autosize action (default unchanged at 30px) and pass minHeight 0 for the tool description so an empty/one-line field no longer reserves the 30px floor and leaves dead space below the text.

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

* chore(ai-agent): regenerate OpenFlow-derived prompts, CLI guidance, and copilot zod schema for tool description

Fixes the check-freshness CI failure (system_prompts + skills.gen.ts) and makes the flow copilot's openFlow.json / openFlowZod.gen.ts aware of the new AgentTool.description field so AI-authored tools can set it.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:33:16 +02:00
hugocasaandClaude Opus 4.8 5a460dbec6 fix: accept bunnative language in AI chat flow step validation (#10030)
* fix: accept bunnative language in AI chat flow step validation

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

* chore: regenerate copilot flow schema from openflow spec

Run gen_openflow_schema.sh + minifiedOpenflowJson.sh instead of hand-patching. Also syncs three fields the checked-in generated files had drifted from since the last regen (reasoning_effort, reasoning_token_delta streaming event, aiagent tag).

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

* chore: regenerate system prompts for bunnative openflow schema

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 09:24:09 +02:00
GuilhemandClaude Opus 4.8 6b01caaf26 fix(ai): flow writer builds approval steps as scripts, not identity (#9985)
* fix(ai): flow writer builds approval steps as scripts with getResumeUrls, not identity

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

* test(ai-evals): accept rawscript or script for approval step type

The flow-writer prompt allows an approval step to be `type: rawscript`
or `type: script`, but the topLevelStepTypes check pinned an exact
`rawscript` match, so a valid `type: script` approval would fail
deterministically. Let the check accept a list of allowed types.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:47:14 +02:00
hugocasaandClaude Opus 4.8 a368d49bd8 feat(ai-agent): support reasoning effort in AI agent workflow steps (#9886)
* feat(ai-agent): support reasoning effort in AI agent workflow steps

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

* fix(ai-agent): round-trip native Anthropic thinking blocks and fix DeepSeek/Mistral reasoning

Address review: native Anthropic now captures the signed thinking block during streaming and replays it before tool_use across iterations (prevents a 400 on multi-turn tool use). DeepSeek 'off' sends thinking:{type:disabled} instead of the rejected reasoning_effort:none, and Mistral drops temperature when reasoning is on.

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

* feat(ai-agent): move reasoning effort into the provider/model selector

Store reasoning_effort on ProviderConfig (next to the model) instead of a separate flow arg, and render the selector inside AIProviderPicker under the model dropdown. Add an explicit 'off' option on models that disable reasoning by omission (e.g. Claude), so reasoning can always be turned off from the UI.

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

* refactor(ai-agent): use DropdownV2 for reasoning effort, matching copilot chat

Replace the Select combobox with the same DropdownV2 action-menu the copilot chat reasoning selector uses. Each option carries an action instead of a bound value, so click selection is unambiguous and there is no typeahead/sentinel-value mismatch on the off/default entries.

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

* chore(ai-agent): regenerate system prompts for ProviderConfig.reasoning_effort

Refresh system_prompts/auto-generated and cli skills.gen after adding reasoning_effort to the OpenFlow ProviderConfig schema (check-freshness).

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

* fix(ai-agent): clear stale reasoning effort on model change; dedup bedrock reasoning folding

Address cubic review: (P1) the reasoning picker now clears the stored effort when the newly selected model doesn't accept it (e.g. carrying 'xhigh' from Opus onto a model that tops out at 'high'), not only when the model can't reason at all. (P3) the proxy's accumulate_reasoning_delta now delegates to the shared bedrock_stream_event_to_reasoning_delta so worker and proxy folding can't drift.

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

* feat(ai-agent): stream reasoning summary and show a thinking affordance in flow chat

Add StreamingEvent::ReasoningTokenDelta, emitted from every worker reasoning path (Anthropic native thinking deltas, Bedrock, Gemini thought parts, OpenAI-compatible reasoning_content, OpenAI Responses reasoning_summary_text with summary:auto). The flow chat parses it and renders a collapsible 'Thinking' affordance on the assistant message (thinking tokens bill regardless of display, so surfacing the summary is billing-neutral).

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

* fix(ai-agent): persist streamed reasoning onto the finished chat message

Reasoning isn't stored server-side, so the completion re-poll (which swaps temp messages for the persisted ones) was dropping the streamed thinking summary. Carry it onto the final assistant message so the 'Thought process' affordance survives the run.

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

* feat(ai-agent): match flow-chat thinking box to the copilot chat reasoning UI

Replace the plain <details> thinking affordance with the same bordered, collapsible reasoning box the copilot chat uses (chevron + Brain/spinner + 'Thinking' header, markdown body, expand-while-streaming/collapse-on-answer).

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

* fix(ai-agent): attribute streamed reasoning per turn by content; drop duplicated comment

Address review: the completion-poll carry-over now matches each temp assistant turn's thinking summary to its persisted message by content, so a multi-turn response (reasoning -> tool call -> final answer) no longer misattributes an earlier turn's thinking to the final answer or drops intermediate turns. Also removes a leftover duplicated comment block above the AIReasoningEffortPicker effect.

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

* fix(ai-agent): address review round 2 (carry-over edges, off-token validity, aria, test)

cubic round 2: (1) reasoning carry-over now consumes temp turns in order verifying content, so identical/empty-content multi-turn responses attribute thinking correctly and reasoning-only turns aren't dropped; (2) the picker's stale-value check only accepts the off token when the model can actually disable reasoning; (3) add aria-expanded to the Thinking toggle; (4) add a test for the failed tool_result path.

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

* fix(ai-agent): add bottom margin below the flow-chat thinking box

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

* fix(ai-agent): don't request OpenAI reasoning summary, matching the copilot chat

OpenAI gates reasoning summaries behind org verification, so requesting summary: auto would 400 for unverified orgs. The copilot chat requests effort only and never asks for a summary; align the worker with it (reasoning: { effort }) and drop the now-unreachable summary-delta parsing. OpenAI reasoning no longer streams a summary in flow chat (consistent with the copilot); Anthropic/Bedrock/Gemini/DeepSeek reasoning display is unchanged.

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

* fix(ai-agent): scope reasoning carry-over to newly persisted messages

cubic round 3: matching by content over the full history could attach a new turn's reasoning to an older message with identical text. Restrict eligible targets to the messages just fetched for this response (via afterSeq), so historical turns are never touched.

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

* fix(ai-agent): carry reasoning newest-first instead of gating on the final poll

cubic round 4: gating the carry-over on the final poll's filteredResponse dropped reasoning for messages already fetched by an earlier streaming poll (their id is excluded by afterSeq). Walk persisted newest-first and consume the newest matching pending summary, stopping once summaries run out. This response's turns are always at the end, so they claim their own reasoning (P1) before older history is reached (P2), regardless of which poll persisted them.

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

* refactor(ai-agent): drop flow-chat reasoning display, keep backend + effort picker

The chat-side thinking box relied on non-deterministically matching streamed (ephemeral) reasoning back onto persisted messages, which kept spawning edge cases. Remove the flow-chat display entirely (ChatMessage box, FlowChatManager carry-over/threading, parseStreamDeltas reasoning) and keep the sound backend: per-provider reasoning-effort requests, thinking-block round-trips for tool calls, and ReasoningTokenDelta streaming. A display can be built on top later, deterministically (e.g. once the stream carries the persisted message id).

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

* fix(ai-agent): include reasoning_effort in default-config compare; document reasoning_token_delta

Codex/Pi nits: isSameAsStoredConfig now compares reasoning_effort so the 'use as personal default' toggle reflects effort-only changes; openflow streaming-events doc lists the reasoning_token_delta event (regenerated auto prompts).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:16:07 +02:00
hugocasaandClaude Opus 4.8 d9b080f57f feat(ai): add Azure AI Foundry as a native AI provider (#9879)
* feat(ai): add Azure AI Foundry as a native AI provider

Adds `azure_foundry` as a new AIProvider variant wired through the AI
chat (copilot) and AI agent flow steps. Foundry's chat completions API
is OpenAI-compatible and uses Azure conventions (api-key header, Azure
URL building), so it reuses the existing OpenAI-compatible query builder
and proxy path via the shared `is_azure` helper (renamed from
`is_azure_openai`).

Backend (windmill-ai):
- New `AzureFoundry` enum variant (serde `azure_foundry`)
- `get_base_url` requires a resource base URL (like Azure OpenAI / Custom)
- `is_azure()` covers Azure OpenAI + Foundry (api-key auth, Azure URL)
- Added to OpenAI-compatible proxy support and HttpForward proxy mode
- New proxy URL unit test

Frontend (copilot):
- New provider entry, completion config, model-token handling, streamed
  usage tracking, and reasoning registry (all model-id-gated, so a no-op
  for Foundry's non-OpenAI catalog)
- Treated as a chat-completions provider, not the OpenAI Responses API

OpenAPI:
- `azure_foundry` added to AIProvider (openapi.yaml) and AIProviderKind
  (openflow.openapi.yaml); regenerated CLI guidance

Note: the `azure_foundry` resource type (base_url + optional api_key) is
hub-managed and must be published to the Windmill Hub separately.

Fixes WIN-2122

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

* fix(ai): add azure_foundry to copilot flow Zod provider enum

The tracked copilot flow schema (openFlowZod.gen.ts and its openFlow.json
source) still carried the old AIProvider enum, so validateFlowModules /
validateSpecialFlowModule rejected AI-generated flow edits that create or
update an aiagent module with provider kind "azure_foundry" before they
could be saved. Add the value to both (preserving the generated single-line
format) and a regression test over the flow-module validation path.

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

* feat(ai): lead provider list with OpenAI, Anthropic, Google AI

Reorder AI_PROVIDERS so the three primary direct providers come first. The
AIProviderPicker renders the first three entries as quick-access buttons, so
these become the defaults (previously OpenAI, Azure OpenAI, Azure Foundry);
Azure OpenAI / Azure Foundry stay adjacent right after. No logic depends on
provider order (only per-provider defaultModels[0] is read).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:42:44 +02:00
Ruben Fiszel a6a5600833 feat(flow): support worker tag override on AI agent steps (#9513)
* feat(flow): support worker tag override on AI agent steps

* refactor: drop ineffective tag passthrough in nested agent tool path

* chore: regenerate openflow-derived system prompt artifacts
2026-06-10 13:54:05 +00:00
hugocasaandClaude Opus 4.8 f2f0812a04 feat(flows): opt-in to include the stopping step's result in early-stop errors (#9446)
* feat(flows): early stop can include the stopping step's result in the raised error

When a step uses Early Stop with "Raise an error message if stopped", the
flow result was entirely replaced with a static error object
({"error": {"name": "EarlyStopError", "message": "..."}}), discarding the
stopping step's own output. This made it impossible to stop+fail a flow
while preserving the data the step produced (e.g. an API that returns
HTTP 200 with a userErrors payload).

Add an opt-in `error_include_result` flag on StopAfterIf. When enabled on
the raise-error path, the raised payload becomes
{"error": {...}, "result": <step result>} instead of dropping the result.
Default is false, so existing behavior is unchanged. The option is threaded
through the worker's stop-after-if handling (including stop_after_all_iters_if
for loops/branchall) and exposed in the flow editor's Early Stop panel.

Fixes WIN-2012

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

* test(flows): cover early-stop error_include_result payload shaping

Add a regression test asserting that a step using Early Stop with a raised
error message and error_include_result=true fails the flow while preserving
the step output as {"error": {..}, "result": <step result>}, and that with
the flag off the result is the bare {"error": {..}} object.

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

* refactor(flows): nest early-stop step result inside the error object

Embed the stopping step's result under `error.result` rather than as a
top-level sibling of `error`. This keeps the flow result shape as
`{ "error": { .. } }` — identical to a normal error — so consumers that
key off the top-level shape (single `error` key) keep working, while the
data is still preserved for those that look inside the error object.

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

* feat(flows): always include the stopping step's result in early-stop errors

Drop the opt-in `error_include_result` gate. Since the step result is nested
inside the error object (`error.result`), the top-level result shape stays
`{ "error": .. }` — identical to a normal error — so consumers that detect or
parse failures by the top-level shape are unaffected. Gating it added schema
surface, plumbing, and a UI toggle for no real compatibility benefit.

Now, whenever a step early-stops with a raised error message, the flow fails
and the raised error embeds the stopping step's own result under
`error.result` (aggregated iteration results for loops/branchall). This
reverts the `StopAfterIf.error_include_result` field, its threading, the
OpenAPI/generated-client surface, and the editor toggle; the "Raise an error
message" tooltip now notes that the step result is included.

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

* feat(flows): gate early-stop result inclusion behind opt-in flag

Re-introduce the per-step `error_include_result` flag (default off) instead
of always embedding the step result. Although nesting the result under
`error.result` keeps the result *shape* backward-compatible, it does not
address data exposure: a failed flow's result is propagated to synchronous
webhook callers, the flow's failure module, and the workspace/global error
handler (commonly a Slack/email/outbound-webhook notifier). Always including
the step output would surface previously-redacted intermediate data to all of
those sinks for every existing error-stop flow.

Gating keeps the existing behavior (bare `{ "error": .. }`) as the default and
only embeds `error.result` when the flow author explicitly opts in, matching
the original issue's intent.

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

* fix(flows): omit error_include_result when false; refresh generated prompts

- Add `skip_serializing_if = "is_false"` to `StopAfterIf.error_include_result`
  so serialized flows are byte-identical when the flag is off. Fixes the
  `flowmodule_serde` round-trip test (cargo_test) and avoids churn on existing
  flows.
- Regenerate `system_prompts/auto-generated/` and `cli/src/guidance/skills.gen.ts`
  for the new OpenFlow `error_include_result` property. Fixes check-freshness.

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

* test(flows): cover error_include_result for the loop "stop after all iters" path

Add a regression test for the stop_after_all_iters_if branch, where `nresult`
already holds the aggregated iteration results — confirming `error.result`
carries each iteration's output (distinct from the per-step fallback path).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 18:57:05 +00:00
Ruben FiszelandClaude Opus 4.8 f0301b1605 feat(flows): preserve step/subflow worker tags under a custom-tagged flow (#9375)
* feat(flows): preserve step/subflow worker tags under a custom-tagged flow

A flow running on a custom worker tag force-propagates that tag to every
descendant step, script and nested sub-flow, overriding their own declared
tags. This made it impossible to route a specific step or sub-flow to a
different worker group. The new opt-in FlowValue.preserve_step_tags lets a
step that declares its own non-empty tag run on it; untagged steps still
inherit the flow tag. Defaults off to preserve existing behavior.

* chore: regenerate system prompts for preserve_step_tags

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

* feat(flows): nest preserve_step_tags toggle under flow worker tag setting

The toggle only affects routing when the flow has a custom worker tag, so
show it as a sub-setting of the Worker Group tag picker, visible only once a
tag is set, instead of as a standalone option.

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

* fix(flows): allow step worker tag picker when preserve_step_tags is enabled

When a flow defines a worker tag, the per-step tag picker was replaced by a
read-only "Flow's WG" label. With preserve_step_tags enabled the step's own
tag is honored, so the picker must remain editable in that case.

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

* fix(flows): propagate preserve_step_tags to branch and loop bodies

payload_from_modules built the synthetic RawFlow for branch/loop bodies with
a default FlowValue, dropping preserve_step_tags. Tagged steps inside a
branch or loop therefore still inherited the parent flow tag even with the
flag enabled. Thread the flag through to the synthetic FlowValue so the
behavior is consistent for nested containers.

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

* fix(flows): clear preserve_step_tags when flow worker tag is removed

Avoids the flag lingering as invisible state after the flow tag (and its
toggle) are removed.

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

* fix(flows): repair preserve_step_tags propagation to branch/loop bodies

The previous commit added flow.preserve_step_tags at the payload_from_modules
call sites but the parameter and FlowValue field were not actually threaded
through (a failed edit left the function unchanged), so the crate did not
compile. This completes the change: payload_from_modules takes preserve_step_tags
and sets it on the synthetic FlowValue for branch/loop bodies.

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

* fix(flows): complete preserve_step_tags propagation to branch/loop bodies

Previous two commits left windmill-worker uncompilable: payload_from_modules
received flow.preserve_step_tags at its call sites but the parameter and the
synthetic FlowValue field were not actually added. This adds the parameter,
sets preserve_step_tags on the synthetic FlowValue, and threads
flow.preserve_step_tags through all five call sites.

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

* fix(flows): clear preserve_step_tags whenever the flow worker tag is removed

The flag was only reset when the Worker Group toggle was switched off, not
when the tag was cleared directly in the picker (or via the YAML editor),
leaving preserve_step_tags=true as invisible state with the advanced badge
still reporting it active. Move the cleanup into the reactive block that
already tracks the flow tag so every clear path is covered.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 12:10:16 +00:00
Ruben Fiszel 9da13d0180 update prompts 2026-05-11 22:34:09 +00:00
centdixandClaude Opus 4.5 4098793db2 fix: split flow prompts for frontend chat (#8968)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 14:56:51 +00:00
eebe24d8b0 feat(cli): wmill dev with per-flow proxy and responsive Dev UI (#8529)
* feat(cli): add `wmill flow dev` subcommand with per-flow reverse proxy and launch.json

Also generates .claude/launch.json for existing flow folders during `wmill init`.

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

* feat: responsive dev layout and hide splitter for single-pane views

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

* fix: clamp flow graph height between minHeight and maxHeight

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

* feat(cli): enhance app new with Claude Desktop integration and better defaults

- Add .claude/launch.json to generated app scaffold for Claude Code preview support
- Add "Open in Claude Desktop?" prompt that creates a CLI session and opens it
  in Claude Desktop Code mode via the claude://resume deep link
- Improve default CSS template with body background, system fonts, and padding

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

* fix(cli): handle both .flow and __flow suffixes in wmill dev

The flow detection in loadPaths only checked the configured suffix
(dotted or non-dotted), so users with nonDottedPaths=true who had
.flow folders (or vice versa) would see inline script edits treated
as standalone script changes instead of flow changes.

Now checks both suffix forms everywhere: type classification,
folder path extraction, path stripping, and loadWmPath lookup.
Also adds raw_app launch.json generation to init and sync pull.

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

* docs(cli): update generated skills with dev workflow and preview commands

Update cli-commands, write-flow, and raw-app skills to document the new
local dev workflow (wmill dev --path, --proxy-port, .claude/launch.json).
Add wmill script preview and wmill flow preview to all script/flow skills
so agents know how to test without deploying.

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

* fix(cli): include path in dev URL and use open.default for browser

- Append &path= to the printed/opened URL when --path is specified
- Use open.default(url) instead of open.openApp for more reliable browser opening

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

* feat(cli): add Claude CLI/Desktop detection hints in wmill flow new

Show contextual instructions for previewing flows based on available tools.

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

* chore: regenerate auto-generated CLI skills for new dev flags

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

* fix(cli): handle mixed flow suffixes in dev file watcher

The ignore() function uses isFlowPath() which only checks the configured
suffix (__flow or .flow), causing files in the other variant to be silently
ignored. Bypass the ignore check for any file inside a flow folder and
force flow type detection regardless of suffix configuration.

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

* feat(cli): drop default proxy in flow folders, open browser, add --no-browser

Manual `wmill dev` in a flow folder should not implicitly enable the
reverse proxy. Both proxy and legacy modes now open the browser; the
new --no-browser flag opts out. Claude Code launch.json templates pass
--no-browser so the IDE preview doesn't fight a system browser window.

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

* feat(cli): gate dev broadcasts by --path and push currentLastEdit on connect

When --path (or auto-detected flow path) is set, drop file events for
any other path so the dev page stays locked to the requested resource
and currentLastEdit can never reflect an unrelated edit. The connection
handler proactively pushes currentLastEdit so the page renders without
waiting for the first file change.

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

* fix(frontend): prefer WebSocket for flow round-trip when wmill dev is connected

updateFlow used isInIframe priority, which routed Claude Code's iframe
preview through postMessage (no listener) and silently dropped flow
edits. Flip the priority: when the wmill dev WebSocket is open, use it
(covers standalone tabs and Claude Code's preview); fall back to
postMessage only when no WS is connected (the VS Code extension's iframe
URL has no `local=true`, so it never opens one). Also stop assigning
lastSent before a channel actually accepted the message, so a CONNECTING
WS doesn't silently swallow the first change.

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

* feat(dev): url is source of truth for path; add workspace file picker

Drops the server-side --path gate added in 3c2d5155e1. The dev page now
filters by its URL's ?path= and the CLI is a dumb broadcaster, which
lets multiple tabs each watch different paths. When the URL has no
?path=, the page asks the CLI for a list of workspace items (flows,
scripts, raw_apps) via a new {type:'listPaths'} WS message and renders
a picker. Clicking a flow or script soft-updates the URL via
history.pushState and loads it; raw_apps surface a hint to use
`wmill app dev` since they don't render here.

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

* feat(dev): picker uses homepage tree view with summaries

Replace the hand-rolled Button-list picker with a TreeView-style layout
that mirrors the Windmill homepage: folder/user tree grouping via
`groupItems`, item rows rendered through the shared `Row.svelte` (no
actions, no favourites, no link — just the visual), a `SearchItems`
fuzzy filter with the same search input styling and placeholder as the
homepage, and `group-open:` chevron toggling on native <details>.

The CLI's listWorkspacePaths now also reads summaries from each item's
metadata (flow.yaml for flows, <script>.script.yaml for scripts) in
parallel so the picker shows summaries as the primary row label, same
as the homepage. Raw apps have no standard manifest so they show the
path only.

Additional polish: title shows "<workspace> (local)" instead of
generic text, subtitle trimmed, item-wrapper owns the border-b so
Row's internal last:border-b-0 doesn't zero it out, summary border
gated on group-open: to avoid doubled lines when a folder is
collapsed.

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

* fix(cli): wmill dev --no-browser was a no-op

Cliffy's `.option("--no-browser", ...)` creates an option named
`browser` (boolean, default undefined) that becomes `false` when the
flag is passed. The previous code checked `opts.noBrowser`, which
Cliffy never populates, so the guard silently no-op'd and the browser
always opened. Rename to `browser` and check `=== false` explicitly,
matching the `wmill app dev --no-open` convention.

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

* feat(dev): picker warns when wmill dev server is unreachable

Track WS state in Dev.svelte (connecting/open/closed) — 'closed' is
set on either the WS error or close event. When closed, the picker
replaces the toggle + search + tree with a warning Alert telling the
user to run `wmill dev` from the workspace root. Toggle and search are
hidden rather than rendered disabled because there's nothing to filter
anyway.

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

* refactor(cli): rename wmill dev --no-browser to --no-open

Match the pre-existing `wmill app dev --no-open` flag. Having
`--no-browser` on one dev command and `--no-open` on the other was
just an oversight from my earlier change. All three launch.json
templates (init, flow new, sync pull) switch to `--no-open`.

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

* feat(cli): wmill init creates root .claude/launch.json for the picker

Adds a workspace-root .claude/launch.json so Claude Code can launch
`wmill dev` from the project root and land on the file picker (no
--path → picker mode). Per-flow and per-raw_app launch.json files are
already generated by the existing scans. Skipped (with a gray log) if
the file already exists, so the user's customizations are preserved.

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

* feat(cli): add skipClaudeAssets wmill.yaml flag

When `skipClaudeAssets: true` is set in wmill.yaml, all generators
that previously wrote Claude-specific assets become no-ops:

- writeAiGuidanceFiles skips CLAUDE.md and .claude/skills/
  (AGENTS.md is still written — vendor-neutral)
- wmill init skips the root .claude/launch.json + per-flow +
  per-raw_app launch.json scans
- wmill sync pull skips the per-flow + per-raw_app launch.json scans
- wmill flow new skips the new flow's .claude/launch.json
- wmill app new skips the new raw_app's .claude/ folder + launch.json

The flag is added to SyncOptions, DEFAULT_SYNC_OPTIONS, and the
generated wmill.yaml template (commented out — opt-in).

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

* feat(cli): wmill init removes existing Claude assets when skipClaudeAssets is set

Re-running `wmill init` with `skipClaudeAssets: true` now removes
previously-generated Claude assets so the workspace state matches the
config. Narrow scope, no confirmation:

- per-flow / per-raw_app .claude/launch.json (each parent .claude/
  collapsed if empty)
- root .claude/launch.json
- .claude/skills/ (wholly ours; safe to remove the subtree)
- root .claude/ collapsed if empty
- CLAUDE.md only if its content matches the default
  ("Instructions are in @AGENTS.md\n"); otherwise left in place
  with a note

Each removal is logged in yellow under a single gray intro line that
prints lazily on the first removal — a clean tree stays silent.

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

* fix(cli): wmill workspace add browser open silently no-ops

`open.openApp(open.apps.browser, { arguments: [url] })` resolves its
Promise even when the OS-level launch does nothing, so the CLI prints
"Opened browser for you" but no tab appears. Same pattern was already
fixed in `dev.ts` by commit 3272c29c2e — use `open.default(url)`,
which delegates to the native URL opener (`open` on macOS, `xdg-open`
on Linux, `start` on Windows) and actually rejects on failure.

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

* fix(cli): wmill init workspace prompt no longer duplicates active profile name

Cliffy's Select.prompt renders `default: X` as `(X)` next to the
question header, which duplicates whichever workspace name the
default points to. Drop `default` and instead reorder the list so
the active profile is first (cursor-preselected by virtue of position)
and append "— active" to its label so the indicator lives where it's
contextually relevant.

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

* docs(skills): expand preview-vs-run guidance for write-flow + all write-script-* skills

Both `wmill flow preview/run` and `wmill script preview/run` have the
same intent split — preview hits the local file, run hits the deployed
version, sync push deploys. The skills' "after writing" sections used
to terse-list the commands and just say "do not run them yourself",
which encouraged the wrong reflex of `sync push` + `run` to "test".

Rewrite the section in both `system_prompts/base/flow-base.md` (drives
write-flow) and the `script_cli_intro` block in
`system_prompts/generate.py` (drives all write-script-<lang>) to:

- explicitly list `preview` as the default for local iteration,
- spell out the few cases when `run` or `sync push` are appropriate,
- offer to test as a one-sentence next step (no multi-option menus),
- mark `preview` as safe to run autonomously.

Regenerate auto-generated/ + cli/src/guidance/skills.ts.

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

* refactor(cli): wmill dev — clearer mode names and accurate startup messaging

- Rename `startLegacyServer` to `startDirectServer`. "Legacy" implied
  it was on the way out; the two modes (proxy vs direct WS) actually
  serve different topologies and both stay. Add comments above each
  section spelling out who they're for: proxy mode for embedders that
  require a localhost origin (Claude Code preview), direct mode for
  standalone browser tabs and the VS Code extension iframe.

- Replace the stale "Dev server will automatically point to the last
  script edited locally" log line. Now print path-aware text:
  - with --path (or auto-detected): "Watching <path> — edits will live
    -reload in the dev page"
  - without: "Open the dev page and pick a flow or script to preview —
    edits will live-reload" plus a hint about --path
  Mirror the same in proxy mode after the listen callback.

- Drop the redundant "Go to <url>" line when --no-open isn't passed
  (maybeOpenBrowser already prints "Opened browser at <url>").

- Rename "Server listening on port 3001" to
  "Dev WebSocket listening on ws://localhost:<port>/ws" so the line's
  purpose is obvious.

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

* refactor(cli): drop per-folder .claude/launch.json generation

Stop creating `.claude/launch.json` inside every flow folder, raw_app
folder, and at `wmill flow new`/`wmill app new` time. The workspace-
root `.claude/launch.json` from `wmill init` stays — it's the picker
entry point and the one place where the deterministic "click → preview"
UX is high-value.

Removed from:
- `wmill init` — per-flow + per-raw_app scans
- `wmill sync pull` — per-flow + per-raw_app scans (also drops the
  now-unused `node:fs` mkdirSync/writeFileSync import)
- `wmill flow new` — bootstrap no longer scaffolds `.claude/`
- `wmill app new` — same; also drops the `.claude/launch.json` lines
  from the post-create directory listing

Skills already give the agent the right CLI commands, so per-folder
launch.json was redundant context. Existing files in user projects
keep working but won't be regenerated; `wmill init` with
`skipClaudeAssets: true` cleans them up.

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

* feat(cli): wmill app new flags + tighten raw-app skill for AI agents

`wmill app new` is interactive by default, which hangs forever when an
AI agent tries to use it. Add flags so the wizard can be bypassed
end-to-end:

- `--summary <text>`, `--path <path>`, `--framework <react19|react18|
  svelte5|vue>` (required for non-interactive)
- `--datatable <name>` (opt into the datatable wizard)
- `--schema <name>` (creates schema with CREATE SCHEMA IF NOT EXISTS
  if it doesn't already exist; only valid with --datatable)
- `--overwrite` (replace existing directory without prompting)
- `--no-open-in-desktop` (suppress the Claude Desktop offer)

Mode is auto-detected: providing any of --summary/--path/--framework
puts the run into non-interactive mode where the datatable wizard,
overwrite prompt, and Claude Desktop prompt all skip silently (or fail
fast on conflict instead of waiting for stdin). Each provided flag is
validated upfront with a clear error message.

Skill side: rewrite `system_prompts/base/raw-app.md`'s "Creating a Raw
App" section so the AI agent knows it should run the command itself
with flags (not tell the user to run it interactively). Direct the
agent to use `AskUserQuestion` with one bundled call to gather any
missing summary/path/framework — refuse to invent values, refuse to
default. Anti-patterns spelled out explicitly.

AGENTS.md template (`cli/src/guidance/core.ts`) had a contradicting
line ("MUST ask the user to run wmill app new in its terminal first")
that was loaded eagerly into agent context and overrode the skill —
replaced with the same agent-driven guidance, pointing to the
raw-app skill for the full procedure.

Regenerate auto-generated/ + cli/src/guidance/skills.ts.

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

* feat(cli): per-target preview launch.json + agent uses wmill flow new

Refactor the agent's dev/preview workflow:

- Drop root .claude/launch.json generation from `wmill init`. Sharing one
  generic entry across sessions caused preview collisions; agents now add
  per-target named entries (windmill: <wmill_path>) on demand.
- New `preview` skill in system_prompts/base/preview.md. Branches on
  whether `mcp__Claude_Preview__*` MCP tools are available: with them,
  add a per-target launch.json entry pinning its own port + --proxy-port
  + --path + --no-open and invoke the MCP preview tool; without them,
  start `wmill dev --path <X> --no-open` directly and hand the URL the
  CLI prints to the user. Never touch launch.json in the direct case.
- Agents must run `wmill flow new <path>` themselves to scaffold flows
  (folder + flow.yaml with the right suffix), parallel to the existing
  `wmill app new` rule. Missing path/summary trigger AskUserQuestion;
  no inventing values.
- write-flow skill: 4-step Creating a Flow procedure that opens the
  visual preview *before* editing flow.yaml so the user watches the
  flow take shape via live reload.
- `wmill flow new` always prints the `wmill dev --path <X>` preview
  hint; drop the Claude CLI/Desktop detection branches.

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

* docs(skills): open app preview before editing in raw-app skill

Mirrors the flow skill's Step 3 — opening `wmill app dev` via the
preview skill before touching App.tsx so the user watches the app
take shape via live reload, instead of seeing the finished result
at the end.

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

* fix(dev): guard WS replaceFlow with lockChanges to prevent echo

The postMessage handler at Dev.svelte:306-312 wraps replaceFlow with
lockChanges = true (cleared 500 ms later) so the $effect on
flowStore.val doesn't immediately re-serialize and re-send the freshly
received payload. The WebSocket handler did not, so on the initial
flow push (dev.ts:568-574 sends currentLastEdit on connect), the
client would echo back to handleFlowRoundTrip, which runs the
orphan-file scan. On content equality the write was a no-op, but the
scan could still delete files the server did not list.

Mirror the same lockChanges/timeout pattern in the WS replaceData
handler. Apply to both flow and script paths for symmetry.

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

* fix(cli): correct wmill dev description + gate broadcasts server-side

Two related fixes:

1. The 'auto-pushes them to the remote workspace' wording in the
   wmill dev description was wrong — the command never deploys, it
   only broadcasts file changes over WS for live preview. Reworded
   to call this out explicitly and point at 'wmill sync push' for
   the deploy case.

2. Move the path filter out of the client (Dev.svelte:491-495) and
   into broadcastChanges. Earlier the filter was client-side with
   the comment 'server stays a dumb broadcaster' even though commit
   3c2d5155 was titled 'gate dev broadcasts by --path'. Doing the
   compare server-side aligns the implementation with the commit
   narrative, cuts WS traffic when --path is set, and keeps the
   per-tab semantics for the picker (each picker tab still gets the
   full 'paths' listing on first connect).

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

* fix(cli): drop dead launch.json cleanup, fix description, narrow orphan scope

Three review fixes:

1. cleanupClaudeAssets removed both root and per-folder
   .claude/launch.json files that this CLI never generates anymore.
   Per the user's "feature hasn't been released yet" guidance, no
   migration is needed — drop the dead scan and the root rm. Also
   drop the now-unused nonDottedPaths argument (and its flowSuffix
   / rawAppSuffix locals).

2. The skipClaudeAssets description in template.ts listed
   .claude/launch.json among the assets it skips, but launch.json
   is no longer generated. Drop it from the description string.

3. The dev round-trip's orphan cleanup deleted any non-dot file in
   a flow folder that wasn't in extractedPaths — including
   README.md, fixtures, TODO.md, etc. Restrict the deletion to
   files whose extension is in a known inline-script set
   (.ts/.js/.py/.go/.sh/.sql/.ps1/.php/.rs/.java/.cs/.r/.graphql).

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

* refactor(cli, frontend): dedupe flow suffix helpers, use UI components

Five small follow-ups from the PR review:

1. dev.ts already had stripFolderSuffix() but three callsites were
   reimplementing the same .flow/__flow if-else inline. Add an
   isFlowFolderName(name) helper next to it and replace the duplicates
   in startProxyServer's cwd check, the file-watcher localPath strip,
   and normalizeWmPath.

2. Dev.svelte:866 was a <div onclick> with two svelte-ignore comments
   for the missing a11y handlers. Replace with a real <button
   type="button"> — kills the warnings, no visual change.

3. Dev.svelte:1283 was a raw <input type="text"> for the module
   summary. Replace with the existing <TextInput> component (same one
   the picker search at :1010 uses), per frontend/CLAUDE.md.

4. Dev.svelte:197 typed relativePaths as any[]; tighten to the actual
   union (string | [number, string])[] — the python helper returns
   tuples, the typescript one returns strings.

5. app/new.ts:822 fired exec("open <deeplink>") with no callback, so
   an OS that refused the URL scheme silently failed and we still
   logged "Opened in Claude Desktop!". Move the success log inside an
   exec callback that surfaces the error and prints the deep link for
   manual opening.

Plus a brief comment above parseWatchPath explaining its resync
contract (initial load + popstate + explicit pickPath, no generic
pushState listener).

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

* fix(cli): regenerate auto-gen for dev description; drop apostrophe to satisfy parser

generate.py:326 extracts .description() with the regex
[^"\']+ which bails on either quote type. Commit ff3a8e4ebd's new
description had an apostrophe inside double quotes ('wmill sync
push'), so the parser saw no description at all and the
auto-generated files dropped the line entirely — which is what
check-freshness caught on origin/main.

Quickest path to green CI: rephrase the description without the
inner apostrophe, then regenerate. The generator's regex is the
real bug but fixing it is out of scope here.

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

* feat(cli): seed app .claude/launch.json before opening Claude Desktop

When the user accepts "Open in Claude Desktop?" in wmill app new, write
a per-app .claude/launch.json (named "windmill: <appPath>") into the
freshly-created app folder before the deep link fires. Entry runs
'wmill app dev --no-open --port ${PORT:-4001}' from the app folder
(which is the cwd Claude Desktop opens with), so the user can hit play
right away to launch the preview.

Skip if .claude/launch.json already exists — never clobber user edits.

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

* fix toggles positions

* fix(dev): gate picker mode on ?local= so VS Code iframe still renders content

The VS Code extension iframe loads the dev page without ?path= and
without ?local=true. After the picker rework, an empty watchPath
flipped pickerMode on, so the page rendered the picker UI even
though the extension was sending replaceScript / replaceFlow
postMessages — leaving the user stuck on the picker forever.

Picker mode only makes sense on the local dev page, where the wmill
dev WebSocket can supply the workspace listing. Anywhere else (VS
Code iframe, plain remote tab) the picker has no data source and no
purpose. Add an isLocalDevPage check so the picker only shows when
?local=true is present.

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

* refactor(dev): mirror vscode extension's processFlowMessage round-trip

Three changes that bring our wmill dev round-trip into lockstep with
the windmill-vscode extension's processFlowMessage in src/extension.ts:

1. New cli/src/commands/dev/pathscript-restore.ts — verbatim port of
   the extension's src/utils/pathscript-restore.ts. Adds AI-agent tool
   walking that the previous local copy was missing (flows with
   PathScript-shaped tools weren't being preserved across round-trip).
   Header comment makes the cross-repo link explicit.

2. handleFlowRoundTrip rewritten to mirror processFlowMessage step-
   for-step: reads failure_module + preprocessor_module from the
   current flow.yaml, passes them to extractCurrentMapping, shares one
   pathAssigner across all extraction calls, extracts inline scripts
   from those special modules too, skips writing files whose content
   starts with !inline (treats as pointer directives), and only
   rewrites flow.yaml when the serialized YAML actually differs.

3. snapshotPathScripts / tagReplacedPathScripts callsites in loadPaths
   were passing the FlowFile wrapper instead of FlowFile.value — the
   helpers walk .modules / .failure_module / .preprocessor_module,
   which only exist on .value, so PathScript snapshots silently
   no-op'd on the file-watcher path. Pass .value at all four sites.

Deliberate divergence from the extension: orphan-cleanup keeps the
INLINE_SCRIPT_EXTS allow-list so README.md / fixtures aren't deleted.
The extension's version still over-deletes; that's tracked separately.

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

* docs(skills): offer visual preview after create instead of auto-opening

Both write-flow and raw-app skills used to instruct the agent to open
the visual preview without asking right after wmill flow new /
wmill app new, on the rationale that live reload is most useful when
the page is already up. In practice this surprised users — opening
the dev page has side effects (browser window pop, possibly a
launch.json entry under MCP-preview Branch A) that warrant consent.

Change Step 3 in both skills from "open it without asking" to "offer
it as a one-sentence next step" — same pattern the same skills
already use for programmatic wmill flow preview offers. Two then-
necessary anti-patterns ("just open it", "open it before editing")
are dropped along with the auto-open instruction.

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

* fix(cli): probe both ip stacks before binding wmill dev proxy / app dev port

Node's default listen() has platform-dependent dual-stack behaviour.
If the requested port is already held on the IPv4 stack, listen() can
silently fall back to binding IPv6-only ([::1]:N). The OS then routes
new localhost connections to the older IPv4 listener, so the user
opens http://localhost:N and sees a stale prior server with no signal
that anything is wrong. Bit us in practice: a leftover wmill dev
--proxy-port 4000 served traffic for a freshly-started wmill app dev
--port 4000.

New helper at cli/src/utils/port-probe.ts probes both 0.0.0.0 and ::
before binding. On collision it walks upward to the next free port
(up to +20) and logs a prominent warning naming the holder when lsof
/ ss can find it:

  Port 4000 is already in use (held by PID 91418 `bun`). Using
  port 4001 instead.

Wired into:
- wmill dev --proxy-port: the resolved port flows into both
  proxyServer.listen() and the &port=N parameter in the redirect
  URL, so they always match. Bind explicitly to 0.0.0.0.
- wmill app dev --port: only when the user passed --port explicitly
  (the default getPort.default(...) path already handles fallback).

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

* fix(dev): pass placeholder via TextInput inputProps not as top-level prop

`<TextInput>`'s top-level Props don't include `placeholder` — native
input attributes go through the `inputProps` field. The previous
`<TextInput placeholder="Summary" .../>` failed `npm run check` with
"Object literal may only specify known properties, and
'\"placeholder\"' does not exist in type 'Props<\"input\">'.".

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

* nit

* fix(cli): sequential port probe + sync dev test regex with renamed log

Two CI regressions on test-linux:

1. port-probe parallel race on Linux. isPortFreeOnBothStacks ran the
   IPv4 and IPv6 binds via Promise.all. On Linux the default is
   net.ipv6.bindv6only=0, so a bind(::, port) socket also takes the
   IPv4 stack on the same port. Concurrent v4 + v6 binds then race for
   v4 — one wins, the other gets EADDRINUSE on a port that is actually
   free. Walks 20 ports up, all fail the same way, throws, child exits.
   Tests that fetch http://localhost:port time out at 60s.
   Doesn't repro on macOS (bindv6only=1 by default — what I tested
   against). Probe sequentially so each bind fully releases before the
   next starts.

2. dev_server.test 1 regex out of sync. Commit 018dc3861a renamed the
   startup log from "Server listening on port N" to "Dev WebSocket
   listening on ws://localhost:N/ws" but didn't update the test, which
   times out at 30s waiting for the old string. Update the regex to
   match the current log.

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

* Update system_prompts/auto-generated/skills/write-script-graphql/SKILL.md

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix(cli): address dev/app PR review — bugs 1-7

Per code review:

1. app new.ts — wrap claude --session-id exec in try/finally so the
   spinner setInterval is always cleared. On rejection control jumped
   to the outer catch and the spinner kept writing \r forever, garbling
   subsequent output.

2. app new.ts — make --overwrite actually wipe the dir before
   re-creating. Previously logged "Overwriting" but only skipped the
   prompt; leftover files from a different framework (e.g. App.tsx
   from a prior react18 install when re-scaffolding as svelte5)
   survived and produced a hybrid scaffold.

3. dev/dev.ts — anchor the flow-folder match on path segments. The
   substring checks (cpath.includes(".flow/") / "__flow/") also fired
   on names like notes_about__flow_design/readme.md. New
   isInsideFlowFolder + findFlowFolderPrefix split on "/" and check
   segment suffixes. Drops the now-unreachable script→flow fallback
   inside the else branch.

4. dev/dev.ts — direct mode also routes through resolveBindPort so it
   detects dual-stack collisions like the proxy mode does. Bare getPort
   only probes one stack, defeating the whole point of port-probe.ts.
   Also bind to BIND_HOST explicitly. Drops the unused getPort import.

5. dev/dev.ts — normalize opts.path once after mergeConfigWithConfigFile.
   broadcastChanges compared against a non-normalized opts.path, so
   --path f/foo/ or --path f/foo.flow silently dropped every broadcast.
   Also pulls normalizeWmPath to module scope (was a closure inside dev()).

6. dev/dev.ts — guard the initial-state ws.send with readyState === OPEN,
   matching the other branches' pattern.

7. dev/dev.ts — typo: "givena" → "given a".

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

* fix(cli): address dev/app PR review — items 8-10

8. dev/dev.ts — derive INLINE_SCRIPT_EXTS from exts so adding a new
   script language to script.ts auto-extends orphan cleanup. Previously
   .gql, .nu, .rb were missing — flows using those languages would
   leave orphaned inline files behind. Excludes .yml because user
   fixtures commonly use it in flow folders, and leaving a stale
   .playbook.yml inline script is preferable to deleting a fixture.
   Keeps .js for hand-written flows that aren't in the exts list.

9. app/new.ts — wrap Claude Desktop install probe + prompt in
   process.platform === "darwin". The probe (ls /Applications/Claude.app)
   and the open command both only work on macOS — the explicit guard
   makes the platform scope grep-able.

10. app/new.ts — switch the deep-link spawn from exec(`open ${shell-
    escaped url}`) to execFile("open", [deepLink]). sessionId is a UUID
    and absAppDir is URI-encoded today so the old form was safe, but
    execFile removes the shell entirely.

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

* fix(cli,dev): cubic review — port-probe error semantics, pluralize spacing

[2] cli/src/utils/port-probe.ts — distinguish IPv6-unsupported from port
collision in isPortFree. Previously every error code returned false,
including EAFNOSUPPORT / EADDRNOTAVAIL on the IPv6 probe when the host
has no v6 stack at all (IPv4-only containers). resolveBindPort would
then walk all 20 ports getting the same error and throw. Treat only
EADDRINUSE / EACCES as "not free"; everything else as free.

[13] cli/src/commands/app/dev.ts — only probe both stacks when binding
to localhost. The dual-stack collision risk is specific to localhost
(which resolves to 127.0.0.1 + ::1); for an explicit IPv4 host there's
only one stack to worry about, so don't move the user's requested port
over a phantom v6 collision.

[14] frontend/src/lib/components/Dev.svelte — pluralize already inserts
a space between quantity and word, so " item" produced "3  items".
Drop the leading space in both call sites.

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

* docs(system_prompts): cubic review — preview args, skill scopes

Source changes in base/ + generate.py, then regenerated auto-generated/
via python system_prompts/generate.py. Per cubic review:

[4]+[7] generate.py — "pick plausible args from the `main` signature"
was language-blind. SQL queries and Bash scripts use $1/$2 positional
parameters, not a main(...) signature. Reword to call out both shapes
explicitly so the wording survives across all 19 generated language
skills (postgresql, bash, mysql, …) instead of just the ones that
happen to have main().

[5] base/raw-app.md — the "CLI Commands" table said "Tell the user
they can run these commands (do NOT run them yourself)" while the
"Creating a Raw App" section above (added in this PR) tells the agent
to run `wmill app new` itself. Carve `wmill app new` out of the table
and add a one-line note pointing back to the create flow, so the
guidance no longer self-contradicts.

[10] base/preview.md — "These print a `Go to <url>` line on stdout"
was wrong for `wmill app dev`, which prints
"🚀 Dev server running at <url>". List both line shapes explicitly and
suggest a loose http:// match for URL capture.

[12] base/flow-base.md — "regenerate lock files for the flow you
modified" misstated the default scope. `wmill generate-metadata`
scans scripts, flows, and apps by default
(see cli/src/commands/generate-metadata/generate-metadata.ts:71-73).
Update wording to call out the default scope and how to narrow it.

Also folds the cubic [1] graphql safety wording (originally a one-off
edit on the auto-generated file in a895db7) back into generate.py
itself, so it survives regeneration and applies to all language skills.

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

* docs(system_prompts): cubic round 2 — language-specific placeholder syntax

Round 1 wording was too narrow:

- "$1, $2 placeholders for SQL queries and Bash" was wrong for MySQL
  (`?`), Snowflake (`?`), MSSQL (`@P1`), BigQuery (`@name`), and
  PowerShell (which uses `param(...)`, not main()).
- The preview-skill URL match said "first `http://...` token" — remote
  workspaces serve HTTPS, so the regex would miss them.

Source-only fixes in generate.py and base/preview.md, then regenerated
auto-generated/ via python system_prompts/generate.py.

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

* fix(flow): track maxHeight in FlowGraphV2 height effect

cubic [3]: updateHeight() reads both minHeight and maxHeight, but the
$effect only tracked minHeight. Changing maxHeight alone (e.g. when a
parent shrinks the cap during a layout transition) left height frozen
at the previously clamped value.

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

* docs(system_prompts): tool-agnostic wording in base/

cubic [11]: system_prompts/README.md says these prompts must NOT
contain tool usage instructions. Three base files violated this:

- base/flow-base.md (4× AskUserQuestion). Worst offender — leaks into
  the frontend copilot via FLOW_BASE in prompts.ts (consumed by
  getFlowPrompt in frontend/src/lib/components/copilot/chat/flow/
  core.ts:1287). Frontend has no AskUserQuestion tool, so the wording
  was both irrelevant and confusing there.
- base/raw-app.md (5× AskUserQuestion + 1× mcp__Claude_Preview__).
  CLI-skill-only but covered by the same scope rule.
- base/preview.md (5× mcp__Claude_Preview__). CLI-skill-only, same.

Replaced with role descriptions: "ask the user (use a structured-
question tool if your runtime has one)" and "a tool that can embed a
localhost URL inside the IDE / chat surface". Kept one mention of
mcp__Claude_Preview__ in preview.md as an illustrative example, since
documentation of one runtime is fine — what's not fine is gating
behaviour on a specific tool name.

Source-only edits, then regenerated auto-generated/ via
python system_prompts/generate.py.

Verification: grep -r AskUserQuestion system_prompts/auto-generated/
now returns nothing. The remaining AskUserQuestion refs in
cli/src/guidance/core.ts are hand-written CLI-only AGENTS.md content
(not part of system_prompts), and Claude Code does have that tool, so
those are correctly scoped.

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

* fix(flow): drop .no-splitter CSS hack, use snippets to skip Splitpanes

cubic [8]: the previous fix for "top pane is empty in aiagent / noEditor
mode" was a CSS rule that hid `:global(.splitpanes__splitter)` inside
.no-splitter. That cascaded into nested splitpanes too — the aiagent
left/right tabs panel (line 1043), the debug-console editor split
(line 877), and the doubly-nested debug panel (line 1472) all lost
their resize handles.

Refactor the layout instead. Extract top-pane and bottom-pane content
as snippets, then conditionally render either:
  - just the bottom snippet (no Splitpanes wrapper) when the top pane
    would be empty (aiagent or noEditor), or
  - the original two-Pane Splitpanes layout otherwise.

This removes the splitter at its root rather than hiding it, so
nested splitters are unaffected. The bottom Pane's complex bind:size
getter/setter (which returned 100 when aiagent) collapses to a simple
binding now that the aiagent path no longer goes through the wrapping
Pane at all.

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

* nit

* fix(flow,preview): cubic round 3 — FlowPathViewer regression + preview skill rewrite

[3149192182] FlowModuleComponent.svelte: my last refactor's
"aiagent || noEditor" condition stripped the FlowPathViewer for
noEditor + type === 'flow', because the top-pane snippet was no longer
rendered. The flow-viewer pane is the only thing that *does* show in
that mode, so it shouldn't have been collapsed. Tighten the condition
to "aiagent || (noEditor && type !== 'flow')".

[3149060930] system_prompts/base/preview.md: Branch A detection was
too broad — "can embed or open a localhost URL" is strictly weaker
than "can read .claude/launch.json and launch a configuration". Only
the Claude Desktop / Code MCP integration does the latter; most
embedders only do the former. Restructure preview.md around two
orthogonal axes:

  1. Mode (proxy vs direct) — driven by "does the embedder need a
     localhost URL?". Direct is the default; proxy is for embedders
     that sandbox cross-origin loads.
  2. Who starts the server — you spawn `wmill dev` yourself, OR a
     launch.json-aware runtime (currently only the
     `mcp__Claude_Preview__*` MCP family) launches it on demand.

The two compose into four common cases (regular browser tab, generic
preview pane, localhost-only preview pane, Claude MCP), each with a
clear instruction. The launch.json/MCP machinery is now scoped to a
single section gated on actually having that tool in your tool list.

Source-only edit in base/preview.md, then regenerated auto-generated/
via python system_prompts/generate.py.

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

* Fix dev step display

* nit

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-04-27 20:14:21 +00:00
centdixandClaude Opus 4.5 9a60ff2e77 feat: add ai agent conversation output control (#8915)
* feat: add ai agent chat output flag

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

* fix: suppress ai agent tool chat messages

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

* refactor: rename ai agent conversation output flag

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

* feat: expose ai agent conversation output toggle

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

* fix: gate ai agent chat tab by chat mode

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

* chore: regenerate system prompts

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

* fix: address ai agent chat review feedback

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-23 14:13:20 +00:00
centdixandClaude Opus 4.5 d3cb0c6220 fix: improve flow chat and benchmark coverage (#8825)
* fix: support special flow modules in evals

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

* refactor: extract shared flow helper logic

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

* fix: make special flow tools openai-compatible

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

* fix: improve flow eval prompts and validation

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

* test: relax flow benchmark overfits

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

* test: record updated flow benchmark history

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

* fix: address flow review findings

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

* refactor: source flow chat special module prompt

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

* fix: narrow rawscript helper return type

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

* refactor: dedupe flow chat prompt guidance

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

* fix: relax flow test10 validation

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-15 16:22:39 +00:00
2d18a68099 feat: add scheduled job deletion with configurable retention period (#8753)
* feat: add scheduled job deletion with configurable retention period

Extends delete_after_use with delete_after_secs to enable configurable
retention periods for job args/result/logs. At completion, jobs can be
scheduled for future deletion via a new job_delete_schedule table,
processed by a monitor task. Supports per-script, per-flow, and
per-flow-step configuration. Backward compatible.

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

* feat: add integration tests, revert query! macros, fix review issues

- Add integration tests for resolve_delete_after_secs, schedule_job_deletion,
  flow-level and module-level delete_after_secs, backward compat
- Revert sqlx::query() back to sqlx::query!() macros for compile-time safety
- Regenerate sqlx offline cache
- Fix FlowModule/NewScript/FlowValue constructions in all test files
- Fix autoscaling_ee.rs for updated script_path_to_payload return type

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

* chore: update ee-repo-ref.txt for autoscaling_ee fix

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

* fix: gate cleanup_scheduled_job_deletions behind enterprise feature

Prevents dead_code warning (which CI treats as error via -D warnings)
when compiling without enterprise feature.

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

* chore: regenerate sqlx cache after merge with main

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

* fix: address review feedback on scheduled deletion

- Monitor: roll back transaction on any cleanup error so schedule rows
  survive for retry on next cycle (instead of best-effort then discard)
- Migration: add FK with ON DELETE CASCADE to job_delete_schedule.job_id
  to prevent orphan rows when jobs are deleted through other means
- Simplify bool-to-Option conversion with .then_some(true)

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

* refactor: stop setting delete_after_use alongside delete_after_secs

No mixed-version deployment scenario exists, so delete_after_secs alone
is sufficient. The backend's resolve_delete_after_secs handles
(None, Some(secs)) correctly without needing delete_after_use set.

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

* refactor: remove delete_after_use from public API surface

Remove delete_after_use from OpenAPI spec, API client, runtime client,
and workspace export. Only delete_after_secs is exposed going forward.

The field remains in Rust backend types with #[serde(skip_serializing)]
for backward-compatible deserialization of existing scripts/flows that
were saved with delete_after_use: true.

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

* chore: update ee-repo-ref to 1d4b7a31fc115d6aba8640f7cd3fd5a01abe6806

This commit updates the EE repository reference after PR #519 was merged in windmill-ee-private.

Previous ee-repo-ref: 9eba09a13b778caafc6ae65098b90e53c91984d3

New ee-repo-ref: 1d4b7a31fc115d6aba8640f7cd3fd5a01abe6806

Automated by sync-ee-ref workflow.

* fix: regenerate system prompts, remove unused import

- Regenerate auto-generated system prompts after openflow schema change
- Remove unused serde_json::json import in test file (CI -D warnings)

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

* fix: insert dummy v2_job row in schedule tests for FK constraint

The job_delete_schedule table has a FK to v2_job, so tests need a
real v2_job row before inserting into the schedule table.

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

* chore: trigger CI re-run

* fix: remove heavy flow integration tests to avoid CI worker contention

The flow integration tests spawn workers that compete for CPU with
the existing relock_skip tests under --test-threads=10, causing
consistent 60s timeouts in CI. Keep only the lightweight unit tests
and DB integration tests.

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

* fix: restore correct ee-repo-ref for our branch

The ref was overwritten to main's EE ref during a rebase. Restore to
our branch's EE commit that includes the autoscaling tuple fix.

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

* chore: retrigger CI on fresh runner

* fix: remove FK constraint from job_delete_schedule to unblock CI

The FK with ON DELETE CASCADE to v2_job may have caused performance
overhead during test DB setup (each sqlx::test creates a fresh DB
with all migrations). Remove the FK — orphan schedule rows are
harmlessly cleaned by the monitor.

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

* ee-ref

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-04-08 04:15:28 +00:00
centdixandClaude Opus 4.5 b960598431 fix: hide deprecated cli metadata commands (#8699)
* fix: hide deprecated cli metadata commands

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

* fix: simplify generate-metadata guidance

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-03 15:38:01 +00:00
a46aa641f9 feat: add R language support (#8263)
* feat: add R language support

Add R as a new supported scripting language in Windmill, following the
same pattern used for Ruby. Includes:

- Backend: ScriptLang::Rlang enum variant, DB migration, tree-sitter-r
  parser crate with tests, WASM parser binding, R executor with NSJail
  sandboxing, job dispatch and signature parsing
- Frontend: language picker, R icon, syntax highlighting, editor bar
  insertions (Sys.getenv, get_variable, get_resource), schema inference,
  init code template, BETA badge
- CLI: .r extension mapping, sync support, bootstrap template

R scripts use `main <- function(...)` syntax, jsonlite for JSON
serialization, and system curl for the Windmill client helper.

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

* feat: add R package resolution and installation

Parse library()/require() calls from R scripts to extract dependencies.
Resolve versions from CRAN, cache lockfiles in pip_resolution_cache,
and install packages to a shared R library cache. The run step sets
R_LIBS_USER so installed packages are available to the script.

- Parser: parse_r_requirements() extracts package names from AST
- Executor: resolve() generates lockfile, install() installs from CRAN
- Worker lockfiles: wire up R resolve for dependency jobs

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

* feat: add nsjail sandboxing for R resolve and install phases

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

* fix: fix R get_variable/get_resource and add sandbox annotation + e2e tests

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

* fix: fix R arg inference with JS fallback parser and get_variable/get_resource

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

* fix flake

* nsjail

* nits

* fix: R install improvements - suppress verbose output, flat lockfile logging, Dockerfile R support, rlimits

- Suppress renv verbose output during resolve and install (controlled by #verbose annotation)
- Filter renv from install list (already loaded, causes noisy restart message)
- Log compact "resolved N packages" instead of full renv.lock JSON
- Add R (r-base, r-cran-renv) to DockerfileFull and DockerfileFullEe
- Use disable_rl for nsjail install config (R compiles from source)
- Reduce default concurrency from 20 to 5
- Add rlang to openflow.openapi.yaml
- Fix MainArgSignature (no_main_func -> auto_kind) after main merge

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

* final

* fix: remove accidental R install from multiplayer Dockerfile

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

* fix: remove R from Windows build and DockerfileExtra

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

* fix: rename R migration to avoid timestamp collision with trigger_filter_logic

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

* all

* fix: R install improvements - suppress verbose output, flat lockfile logging, Dockerfile R support, rlimits

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

* fix: add clear error when Rscript binary is missing

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

* fix: fix type errors in R fallback parser, use format! in wrap(), add R system prompts

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: pyranota <pyra@duck.com>
2026-04-01 06:11:37 +00:00
centdixandClaude Opus 4.6 e44504c6e9 feat: add PDF input support to AI agent (#8525)
* feat: add PDF input support to AI agent with user_attachments field

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

* test: add integration tests for PDF input and backward compat

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

* feat: add ContentPart::File variant for PDF support across all providers

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

* refactor: address review feedback on PDF support

- Extract parse_data_url_bytes and mime_to_document_format helpers in Bedrock
- Add is_document_mime helper in ai_types for centralized MIME routing
- Extract s3_object_to_content_part helper to deduplicate image_handler/openai
- Rename AnthropicImageSource to AnthropicBase64Source
- Derive Bedrock DocumentFormat from MIME type instead of hardcoding Pdf

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

* fix: merge user message and attachments into single message for Bedrock

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 13:55:10 +00:00
GuilhemandClaude Opus 4.6 81eb446eee feat: flow group nodes with collapsible groups (#8075)
* feat: add flow group nodes core infrastructure

Add group data model (start_id/end_id boundary pairs), GroupEditor for
CRUD operations, groupDetectionUtils for membership computation and
validation, GroupedModulesProxy for reactive sync, and compound layout
support. Update openflow.openapi.yaml with group schema.

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

* feat: add group UI components and rendering

Add GroupOverlay with bounding box and z-ordering, GroupHeader with
StepCountTab and ellipsis menu, GroupNodeCard, GroupNoteArea for inline
markdown notes, CollapsedGroupNode/CollapsedSubflowNode for collapsed
rendering, GroupEndNode/GroupHeadNode boundary markers, and group
actions in NodeContextMenu and SelectionBoundingBox.

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

* feat: integrate groups into flow graph, builder, and existing components

Wire group support into FlowGraphV2 (overlays, collapsed rendering,
group-aware layout), graphBuilder (GroupedModule tree, container
collapse/expand, group boundary nodes), BaseEdge (drop targets for
group operations), ModuleNode (collapsed container rendering), and
flow map components (schema item grouping). Remove SubflowBound in
favor of CollapsedSubflowNode.

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

* fix: remove banned $bindable(default) pattern and dead ternary

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

* refactor: decouple collapse state from grouped module tree

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

* feat: pass groups prop to FlowGraphV2 and use GroupDisplayState via graphContext

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

* refactor: remove group membership system, compute nesting depth from visual bounds

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

* refactor: simplify GroupOverlay bounds, remove unused headerY and showNotes prop

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

* fix: populate innerNodeIds for expanded subflow overlay

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

* refactor: remove expanded subflow overlay feature for separate PR

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

* fix: flatten groups in getContainerModules to prevent crash on collapsed containers

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

* feat: add drag-to-move support for group nodes

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

* fix: derive group boundaries from expanded membership to prevent splitting existing groups

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

* fix: catch group validation errors and display as flow graph alert

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

* test: add unit tests for group validation in buildGroupedModules

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

* fix: reject virtual nodes (Input, Result, Trigger) from groups

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

* test: add virtual node rejection tests for buildGroupedModules

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

* fix: exclude preprocessor and failure module from groups

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

* fix: disable Create group button when preprocessor is selected

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

* fix: reject selection entirely when it contains excluded nodes

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

* refactor: remove unnecessary excludeIds from buildGroupedModules

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

* fix: remove debug console.log from FlowGraphV2

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

* fix: use cross-browser CSS grid trick for group summary input auto-sizing

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

* fix: hide group boundary edges and reformat GroupNoteArea

Hide edges between group header and first node, and between last node
and group-end, keeping them in the DOM but visually hidden.

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

* refactor: stop FlowGraphV2 from reading groups via groupEditorContext

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

* feat: show module previews with status, selection, and suspend popover in collapsed groups

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

* refactor: extract collapsible implicit containers to separate branch

Remove collapse/expand functionality for implicit containers (forloops,
while loops, branches) from this branch. Backed up as
collapsible-implicit-containers-backup for later rebase.

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

* refactor: use original reactive modules for graph node data instead of proxy snapshots

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

* fix: prevent node loss when moving into forloop inside a group

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

* refactor: replace GroupedModule proxy with structure-only FlowStructureNode tree

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

* fix: use "group-" prefix for group IDs instead of "note-"

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

* fix: update group boundaries when renaming a module ID

When a module at a group boundary (start_id or end_id) is renamed,
the group definitions now get updated before the reactive rebuild,
preventing stale references that would break the flow structure.

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

* fix: update graph layout when removing a group note

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

* fix: add opaque background behind test run button to prevent see-through

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

* fix: detect and reject duplicate group IDs

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

* fix: simplify group creation validation with early marker normalization

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

* fix: use $state.raw in MiniFlowGraph to avoid xyflow performance warning

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

* refactor: address code review feedback

- Revert backend traverse_modules change (not part of this feature)
- Use Map for node lookup in GroupOverlay (O(1) vs O(n) per group)
- Extract computeNodeExtraSpace to nodeExtraSpace.ts for testability

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

* refactor: address PR review feedback

- Compute group depths from structure tree O(n) instead of O(n²) bounds comparison
- Remove unnecessary $derived(groups) in GroupOverlay
- Remove unused collapsed field from container types in OpenAPI spec
- Use NODE.width constant in GroupNodeCard instead of hardcoded 275px
- Add comment explaining intentional stale preservation in rebuild()

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

* fix: preserve flow groups during dependency job re-serialization

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

* fix: resolve Svelte state_referenced_locally warnings in GroupHeader and FlowGraphV2

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

* feat: show subflow groups when expanding a subflow in the graph

- Store both modules and groups when expanding a subflow
- Pass groups to buildStructureTree so group nodes render
- Include subflow groups in overlay rendering and collapse tracking
- Clone modules for prefix rewriting to avoid state_unsafe_mutation
- Register expanded subflow modules in moduleMap before prefix rewriting
- Disable group editing in expanded subflows and read-only views

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

* fix: restore accidentally removed code from main

- Restore subflowBound selection handling in selectionUtils
- Restore comments in SelectionBoundingBox
- Restore deletable={false} in FirstStepInputs

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

* refactor: remove redundant adjacency check from MoveManager

The disableMoveIds check already prevents all invalid drop targets,
making the adjacencySourceId/adjacencyTargetId fields unnecessary.

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

* chore: regenerate auto-generated files after OpenAPI schema change

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

* chore: regenerate cli skills after main merge

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

* fix: include groups in view_graph localStorage state

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

* refactor: centralize canCreateGroup and replace group note with group creation

- Add canCreateGroup StateStore to GroupEditorContext, computed in FlowGraphV2
- Replace "Create group note" with "Create group" in FlowSelectionPanel
- Remove "Add note" from selection bounding box dropdown
- Remove unused NodeContextMenu component
- Wire createGroup through FlowModuleSchemaMap → FlowGraphV2

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

* fix: reject groups spanning parallel branches and surface ill-formed group errors

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

* chore: regenerate auto-generated files after main merge

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

* fix: ensure modules appears before groups in YAML export

Svelte 5's $state proxy registers groups as a tracked property before
it's explicitly set, causing it to appear before modules in Object.keys
iteration. Reorder the value object at export time for readable YAML.

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

* refactor: address second round of PR review feedback

- Add comment explaining duplicateMultiple bypasses structure tree
- Add warning log for inverted ranges in computeGroupModuleIds
- Use NODE.width constant in CollapsedGroupNode instead of hardcoded 275px
- Simplify redundant condition in getGroupsEmptiedBy

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

* refactor: remove stored group ID, derive ephemeral key from start_id:end_id

Groups no longer store an `id` field. Instead, a `groupKey(g)` helper
derives an ephemeral key from `${start_id}:${end_id}` at read time.
This simplifies the schema while preserving all runtime functionality.

When boundaries shift (module deletion), runtime state (collapse,
note heights) is remapped to the new key via GroupDisplayState.remapGroupKey.

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

* feat: add note button, save/cancel hints, and rename collapsed_by_default to autocollapse

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

* fix: propagate selection from collapsed group badges to external listeners

Pass eventHandlers to GroupModuleIcons so clicking a module badge
calls both selectionManager.selectId (visual highlight) and
eventHandlers.select (side panel propagation via onSelect).

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

* chore: regenerate auto-generated files after main merge

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

* fix: hide In/Out popovers and actions during click-to-move

Replace isDragging with isMoving derived that covers both drag-move
and click-move states, disabling popovers, delete button, and test
run button during any move operation.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:47:33 +00:00
5d1c54d9b3 feat: Debounce node (#8324)
* Debounce node works

* sqlx prepare

* sqlx prepare

* fix: address PR review issues for flow node debouncing

- Add sibling check in parent-walking loop to avoid killing branchall siblings
- Remove stale .sqlx cache files from earlier iterations
- Remove single-variant FlowNodeDebounceResult enum, use Result<()>
- Parse flow value once in version guard, recurse into nested modules
- Fix Svelte reactivity when switching selected flow modules
- Fix Tab indentation in FlowModuleComponent
- Use integer types in OpenAPI spec for debounce fields

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

* ee repo ref

* nit sqlx

* add Debouncing: None

* ee repo ref

* ee repo

* sqlx update

* fix: reject node-level debouncing inside branches (branchall/branchone)

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

* Revert "fix: reject node-level debouncing inside branches (branchall/branchone)"

This reverts commit fa4820dde2.

* ee repo

* sqlx prepare

* sqlx prepare

* feat: add MIN_VERSION_SUPPORTS_NODE_DEBOUNCING (1.658.0) version guard

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

* docs: mark node-level debouncing as EE only in openflow schema

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

* fix: guard node debouncing against parallel steps (len > 1)

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

* generate system prompts

* system prompts

* chore: update ee-repo-ref to c04f3851c03758662e4936ff4b6e71bc56dbae7e

This commit updates the EE repository reference after PR #451 was merged in windmill-ee-private.

Previous ee-repo-ref: d140bb8944dfe3efb23cf8c12f556eacf30e2f87

New ee-repo-ref: c04f3851c03758662e4936ff4b6e71bc56dbae7e

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-24 09:47:41 +00:00
hugocasaandClaude Opus 4.6 1503bf948e fix: stop_after_if with empty error_message prevents flow from stopping (#8464)
* fix: stop_after_if with empty error_message no longer prevents flow from stopping

When skip_if_stopped=true and error_message="" were both set, the flow
would continue executing instead of stopping because the empty string
was converted to a default error message, which triggered the error
handler path. Now skip_if_stopped takes precedence and the two options
are treated as mutually exclusive in both backend and frontend.

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

* chore: regenerate system prompts after openflow schema change

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 20:04:51 +01:00
Ruben FiszelandClaude Opus 4.6 b1b9c984e3 make wmill init generated skills respect nonDottedPaths config (#8377)
* docs: add nonDottedPaths convention to CLAUDE.md

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

* docs(cli): update generated skills to use non-dotted path conventions

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

* fix(cli): make generated skills respect nonDottedPaths config

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

* fix(cli): inject nonDottedPaths placeholders in generate.py for skills.ts

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

* fix: default system prompts to non-dotted path conventions

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 05:05:31 +00:00
centdixandClaude Opus 4.5 7fb729cc84 fix(cli): instruct agent to tell user about generate-metadata and sync push instead of running them (#8318)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-12 10:18:03 +00:00
centdixandClaude Opus 4.6 b40cf80fdd fix: optimize flow lock generation and add rt.d.ts guidance for TS resource types (#8295)
Instruct AI to pass specific flow folder path to `wmill flow generate-locks`
instead of running it on all flows. Also add guidance for TypeScript language
files to check `rt.d.ts` for available resource types before using them.
Re-ran generate.py to propagate changes to all auto-generated files.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 13:03:21 +00:00
a7ce5484b8 feat(local-dev): create Claude skills when doing wmill init (#7699)
* use skills

* add prompts

* update system prompts

* generate skills on init

* add prompts in cli

* better for raw apps

* nit

* test pipeline draft

* better

* yaml for triggers and schedules

* cleaning

* better

* add descriptions to ai agent fileds

* adjust

* better openapi

* better

* nit

* feat: add typed provider and memory schemas for ai agent in openapi

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

* feat: improve zod validation errors with dynamic schema extraction

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

* regen

* fix

* cleaning

* refactor: deduplicate skill descriptions in generate_skills_ts_export

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

* cleaning

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-02-05 13:48:32 +00:00
centdix 31e002ad41 feat(ai): standardize and improve system prompts (#7346)
* init

* test in frontend

* copy files

* use in cli

* better

* add desc to sdks

* better

* fix ts parsing

* add docs to ts client

* add docs to python client

* use script prompt in frontend

* regen

* use in flow

* rm

* use in cli, create AGENTS.md instead of cursor rules

* remove apply

* better

* better

* simplify cli

* more docs

* cleaning

* update readme

* generate cli file

* better folder names

* fix ts

* fix multiline
2025-12-12 17:26:19 +00:00