A chat reopened while its flow is still running has to find the message
that started the turn: it carries the flow job, which is the only thing
that knows whether the run is over. An agent writes a row per round and
per tool call, so that message sits an unbounded number of pages back in
the transcript, and a client reading towards it is searching with no end
it can name.
`message_type` answers it in one request. The filter is applied inside
the paginated subquery, so `page=1&per_page=1&message_type=user` really
is the newest user row rather than the newest row that happens to be one.
`MessageType` carries a fourth kind, `System`, that nothing writes and
the Postgres enum has no label for. A request to filter on it is refused
before the database, so what reaches the caller is a word about their
request rather than an error about an enum.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A turn was not a thing. What one is made of lived in three places — a
reactive status record, a non-reactive runtime map, and locals inside the
function following the job — so the question every piece of a turn's work
has to answer, "am I still the turn this chat is on?", had no owner. Each
await site answered it again from whatever abort flag it had in scope, and
each new await needed another answer; the counter this replaces was the
eighth such answer in as many review rounds.
`Turn` holds the run, the handle that stops it, the cursor its stream
resumes from, the rows it is writing and the timers it armed, and the chat
keys one per conversation. Work carries its turn and passes `isCurrent`
before writing, so a frame buffered before a Stop, a poll dispatched a tick
ago, or a settle that outlived its own turn all land nowhere instead of in
whatever replaced it. Ending a turn is one call rather than an abort, two
timers and a pair of reveals that every caller had to remember, and the
sites that release one say which turn they mean.
The chat itself is keyed on its flow and workspace, so changing flow builds
a new panel rather than re-pointing the old one. That is what makes the
guards removable: nothing survives a flow change to need them.
Fixes found while doing this, each of which the old shape allowed: a
two-minute polling deadline that outlived its turn and stopped the next
one's reading; a run launched into a stopped turn that nothing could name;
a conversation-list refresh on every poll tick of a new chat's first turn;
a failed page walk that freed a chat whose run it never got to ask about;
and a Stop during a launch that tore down the turn that came after it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A poll with no row to resume from sent no cursor at all, and the messages
endpoint answers that with the newest page rather than the oldest, so the
rows before it were never read and the sweep dropped the temp rows covering
them. Sequences start at 1, so reading from 0 puts every request on the
forward branch.
A read that stops at the request cap is now kept: each batch is a run of rows
from the cursor, so the next tick carries on from where it stopped. The last
poll of a turn has no tick after it, and there its prefix is dropped instead,
since it would sit beside the temp rows showing the turn's start twice.
`startPolling` armed a two-minute timeout it never held onto, and `stopPolling`
cleared only the interval. A turn ending inside two minutes left the timeout
armed to fire into the next turn on the same conversation and stop its polling
part-way, which a non-streaming turn feels as intermediate rows drying up. The
handle now lives on the turn's runtime and is cleared with the interval.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The counter was the loop variable, which leaves the cap exit one past the last
request and the stall exit on it — so the same expression printed 21 pages for a
cap of 20, in the one message whose job is to say which exit stopped it. It
counts requests that completed.
The constants shared a doc comment that described the cap while sitting on the
page size, and credited it with catching a cursor that stops advancing, which is
a separate check. Each says what it is.
The partial-read rationale is four lines rather than ten, and points at
`loadMessages` for why a reload is the recovery.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment offered the next turn's poll as a recovery route. It is not one: the
cursor comes from the last persisted row, and applying none of them is exactly
what leaves it where it was, so the next poll reads the same window against a
conversation that has only grown. A reload is what recovers, and the comment says
so — it changes how the trade-off reads.
The warning blamed the page cap for both exits, so a cursor that stalled on the
second request reported twenty pages. It names which one stopped it and where it
got to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Keeping the temp rows on a capped read was half the answer: the rows that were
read got appended anyway, so the reader would have seen the same range twice with
the tail still missing. Reading that stops before the conversation does is not a
picture of it, so none of it is applied — the transcript keeps what it has, the
next turn's poll resumes from it, and a reload refetches.
A cursor that stops advancing takes the same route. It was marked a whole read,
which is the opposite of what it is: a full page came back, so rows almost
certainly remain. Unreachable against a backend that filters on the cursor, but
the two guards should not disagree about what they mean.
The warning carries how far it got, since the conversation id alone does not say
how far behind the transcript is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stopping at the cap left the conversation looking fully read, and the sweep then
dropped the temp rows standing in for everything beyond it — the same vanished
answer the paging was added to prevent, one order of magnitude up. The cap is a
guard against a cursor that stops advancing, not evidence the conversation ends
there, so a poll that hit it keeps its temp rows and says so.
The paging test now pins the cursor as well as the rows: a second read that asked
for the same page again would have looked right from the rows alone.
Moves the attachment test out of the middle of another test's comment, which it
had split across two unrelated cases.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two attachments can arrive under one name — a reader picking `report.pdf` from
two folders. Keyed on the name alone they raced to a single object, so the agent
read whichever landed last twice and never saw the other. Each gets a segment of
its own inside the turn's prefix; the name stays the last segment, so anything
reading a name off the key still sees what was attached.
The messages endpoint answers oldest-first with a limit, so one request only ever
reached the start of a turn that wrote more rows than a page — an agent calling
several tools a round. Its answer was among the rows left behind, and the sweep
then dropped the temp rows standing in for it, so the answer vanished until the
conversation was next loaded. The poll reads on until a page comes back short.
Leaving uploaded objects behind when a send does not run is deliberate, and now
says so: deleting them is a request that can fail on a path already failing, and
a resend uploads its own, so a lost send costs one prefix rather than a growing
number.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The streaming path had the same unguarded gap the settle path was fixed for: its
final poll is awaited and then `endTurn(settled)` runs regardless. Settling
releases the conversation's queue, so a message typed before a re-point was
started against the flow now loaded, writing it into the previous flow's
conversation and agent memory — the thing the forgetting exists to prevent.
The poller and the sidebar's loader were unguarded too. Both are guarded at their
own await now, which covers every caller rather than each call site.
The editor chats keyed off `$initialPathStore || $pathStore`, which still falls
back to the typed path on a flow that was never deployed — and there the summary
field rewrites the path on every keystroke, so naming a new flow emptied the
composer as you typed. They take the editor's stable identity instead, which is
also what a preview run records.
`FOR UPDATE` over several rows takes them in whatever order the plan plans, so
two purges could acquire two conversations in opposite orders. Both sites order
by id. The transaction the lock depends on is now stated on `delete_jobs`, which
takes a bare connection and would silently lose the serialisation on one that
autocommits.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clearing the state was not enough: a transcript fetch, a latest-conversation
lookup or a settle tail started before the chat was re-pointed resolved after it
and wrote the previous flow's rows, selection or status straight back — the bug
the clearing exists to prevent, reached through the gap instead.
Each of those now carries the generation it started in and drops what it fetched
if the chat has moved on since.
The re-point test was asserting two things that could not fail — an empty rows
map and an empty conversation list, both empty either way — so it pinned half the
fix. It loads rows now and checks they are gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Widening `cleanup()` made it destructive on a prop the editor changes far more
often than the flow does: both editor chats were passed `$pathStore`, which is
bound to the rename field, so the teardown that forgets a flow could run while
someone renamed one. They take the saved path instead, as the preview drawer's
other two call sites already do, so forgetting happens when the flow does.
The conversation cleanup in `jobs_export` is the same logic as retention's and
had the same race: it now takes the conversation rows before asking whether they
are empty. One lock order across both paths — messages, then conversation.
The lock reads rather than executes because a checked `SELECT` has no `execute`;
the discarded ids are the cost of the compile-time check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three separate holes, all in code this PR added.
`endTurn` cleared every flag but `isDispatchingTurn`, so Stop could not release the
hold a failed transcript load leaves behind. That flag gates the whole surface, so
a conversation whose rows never load took New chat and every other chat with it.
The route component is reused between two flows, so the chat is re-pointed rather
than rebuilt, and `cleanup()` only ended turns. The selection survived, so
`selectLatestConversation` found one already open and the new flow rendered — and
would have sent into — the previous flow's conversation and agent memory. Cleanup
now forgets what it holds about a flow.
Two `delete_jobs` calls each removing one of a conversation's last messages could
not see each other's uncommitted deletes, so neither collected the conversation
and nothing tried again, stranding the row and its `ai_agent_memory`. The
conversation rows are taken before the emptiness check, which serialises them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#11134 landed squashed, so main carries its changes while this branch carries its
commits. The flow chat components resolve to this branch's, as they did when the
branch was stacked on it: the SDK keeps serving external frontends and raw apps,
and the in-app chat runs on these components.
`agentFormFields` takes main's version — it grew `seed` and the `enabled_tools`
rule there, and this branch never edited it — with `agentStreamingEnabled` and
its tests grafted back, since FlowInput, FlowPreviewContent and the flow page all
call it. `runInput` stays removed; nothing reads it.
Brings the EE pin main already moved to, which is what the backend checks were
failing to compile against.
* refactor: run the flow chat UI on the windmill-chat sdk
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix: structural answer check, poll option and latest run in the chat sdk
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix: count jobless tool rows and the loaded license in the flow chat
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Two holes in the window between opening a chat and its job answering.
Stop is on offer for that whole window, and it cancels whatever `status.jobId`
holds — which was not set until the answer came back. Pressing it then cancelled
nothing, ended the turn and freed the chat while the run carried on. The run is
named before the question, and unnamed again if the turn is not taken over.
A load that failed used to release the hold, on the reasoning that an empty
transcript has nothing to interleave with. It does: the run owns the
conversation's agent memory whatever the browser managed to fetch. The hold
stays; selecting the chat again retries the load, and Stop is the way out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The same rule the streaming path already follows, applied to the three places
that still decided for themselves:
- Non-streaming turns waited on `waitJob`, which stops answering without
settling: on its fifth failed status request it cancels and returns, leaving
its promise pending, and the chat busy for the session. They wait on the job
the same way a stream-less turn does, and `pollJobResult` goes with it.
- A Stop whose cancel request failed still ended the turn, freeing the composer
while the run it could not reach carried on. The turn is left to its job.
- Loading a conversation's rows was not covered: between opening a chat and the
job answering, a send was accepted. The hold now spans the load and the
question, and a failed question is asked again rather than taken as a no.
A turn with more rows than a page still has no user row on the page to name its
job, so it is not picked up. That needs the server to say which turn is running.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: return an ai agent step's thinking in its job result
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs: say when an agent result carries no reasoning
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The client was deciding whether a turn had ended from whether its own stream was
alive, which it cannot know. It asks the run instead.
Nothing in the browser survives a reload, so a chat opened while its flow was
running read as idle and the composer took a message — two turns writing one
agent memory. The row the server writes when a turn starts carries its flow job;
that job is asked whether it is over, and if not the turn is picked back up and
followed. The chat is held from the moment the question is asked rather than from
the answer, since a send accepted during that round trip is the thing this
prevents.
A stream resumed this way replays the rounds an agent has already persisted, and
the transcript cannot tell a replayed round from a second one, so the turn's own
rows are dropped before re-attaching; the final poll brings them back.
With that, a turn whose stream dies no longer needs a rule for giving up: it
polls its job until the run says it is over. The retry cap, the cancel fallback
and the settled/unsettled split go — a run that cannot be reached has said
nothing, and Stop is on screen throughout for the reader who wants out.
A turn with more rows than a page has no user row on the page to name its job and
is not picked up; finding it needs the server to say which turn is running.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An API that stops answering says the status is unknown, not that the run stopped,
and the run holds the conversation's agent memory. Marking the turn settled there
both freed the composer and flushed the queued message into a flow that might
still be writing.
The run is cancelled instead, which is what makes the chat safe to use again. If
even that cannot be delivered the turn ends unsettled: the chat is usable, but
its queue waits for the reader rather than going out beside a live run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fallback awaited `waitJob`, which stops answering without settling: on its
fifth failed status request it cancels the run and returns from its inner poll,
leaving the outer promise pending. A turn handed to it never reached `endTurn`,
so the chat and its queued message stayed locked for the rest of the session.
The turn now waits on the job through the client it already has, and always
settles — either the run completes, or it stops answering enough times to call
it unreachable. `waitJob` itself is left alone; its other callers are outside
this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Running out of stream attempts freed the composer while the flow kept going, so
the next turn wrote the same agent memory — the thing the retry was added to
prevent, only deferred. The turn goes to `pollJobResult` instead, which keeps the
chat busy until the run reaches a terminal state and settles it on the rows;
`waitJob` cancels the run rather than waiting forever if the API stays down.
The budget is counted since the last attempt that delivered something, so a long
turn that blips once an hour is not treated like an endpoint that has gone.
An abort is checked inside the consumer loop: the SSE reader drains the frames it
has already buffered, and `endTurn` has by then reset the transcript state they
would be applied to.
The give-up toast truncates what it quotes — a 502 body is often a whole HTML
error page.
Drops the `flow_stream_job_id` scaffolding from the re-attach test: `followJob`
never surfaces that id, so the assertion could not fail for the reason its
comment claimed. Stop cancelling the flow rather than the streaming step is
verified against a running backend instead. Also drops the EventSource stub the
test no longer needs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`followJob` absorbs the server ending a stream on its own clock, but not the
request to open one failing — a restarting server, a 502. Ending the turn there
unlocked the composer while the flow kept running, and the next turn would write
the same agent memory.
Those attempts are retried from the offset already read, so the answer resumes
rather than replaying, and the turn stays busy across the gap. Bounded, so a
genuinely gone endpoint still reports itself instead of retrying forever under a
chat that looks live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(worker): bound object-store cache transfers and relative import fetches
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(worker): bound the codebase download and label slow-step warnings
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(worker): log a stalled cache transfer once and ignore a zero cache timeout
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The flow chat kept its own EventSource loop: re-attach on the server's stream
timeout, resume from `stream_offset`, drop the offset when a retried step gets a
new stream job. `windmill-chat` does the same thing in `followJob`, which this
PR's base makes importable, so the transport moves there and ~190 lines go.
`followJob` is scoped to one job and holds no chat state, so turns still run in
several conversations at once — each drives its own generator, with an
AbortController in TurnRuntime where the EventSource was.
Three things it does that the loop did not: it buffers a chunk that ends
mid-line, where ours parsed the halves separately and dropped the token; it
reconnects on a close that carried no timeout frame, where ours ended the turn;
and it starts a fresh parser when the streaming step changes.
Stop now cancels the flow job rather than the streaming step. `followJob` never
surfaces `flow_stream_job_id`, so the line that overwrote `status.jobId` with it
goes — which is what the comment on the line that sets it always said should
happen, and what keeps the steps after the agent from running on.
`poll_delay_ms` is sent only with an enterprise licence; a CE server ignores it
and warns on every poll.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The flow chat ran on its own components; this points it at the ones the AI
session chat already uses, so the two surfaces share a transcript, a composer
and a sidebar instead of keeping two of each.
The seam is `ChatViewHost` (copilot/chat/chatViewHost.ts): the view components
read the host rather than `AIChatManager` directly, and `getChatViewHost()`
falls back to the session manager, so the copilot's call sites are unchanged.
`FlowChatViewHost` is the second adapter, over `FlowChatManager`.
What the flow chat gains from the move:
- turns running in several conversations at once, with a status, a queue and a
Stop per chat, and an unread count on the rail
- attachments, uploaded to the workspace's object storage for the worker to read
- a composer that speaks for the agent steps a message is fed to: the model, the
thinking effort, and the flow inputs the agent reads straight out of
`flow_input`; everything else is asked for in a Configure-inputs modal
- tool cards with the call and the result, the model's reasoning, and a step name
per answer once a conversation holds more than one agent
- Retry, which replays the failed turn's own run arguments read back from its job
- named and renamable conversations, and test chats kept out of the deployed
flow's list
Backend: conversation rows carry an MCP tool's call and result and the model's
reasoning, which live nowhere else; every row gets a job so retention can empty
it; and the providers parse a non-streaming answer's reasoning.
Stacked on #11134, which keeps the windmill-chat SDK for external frontends and
raw apps while the in-app flow chat runs on these components.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* 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>
* feat: label resource types and integrations with hub display names
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: load hub integration names in the app and flow pickers
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: load hub resource type names where drawers title a type
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: store resource type display names and drop the hardcoded list
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: leave display_name out of the fork comparison
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: ignore over-long synced display names, move name loaders
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: share the hub integration list cache, backfill admins only
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep a name over a nameless duplicate, retry failed hub reads
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(git-sync): run auto-pull as the admin who enabled it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(git-sync): audit the admin grant fork pulls make
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: bump ee ref for the post-commit fork grant audit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(git-sync): address review nits on the auto-pull stamp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to ccada062c072d7b74894b63863728fd1ef9bdffd
This commit updates the EE repository reference after PR #799 was merged in windmill-ee-private.
Previous ee-repo-ref: 7cee30f0cf12721cba551cd754dc817444810470
New ee-repo-ref: ccada062c072d7b74894b63863728fd1ef9bdffd
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: skip instance group members that are not email addresses
* fix: keep provisioned members whose address only proper_email accepts
* fix: judge instance group members by a mirror of the usr email constraint
* fix: fold ascii only in the proper_email mirror, like the constraint
* fix: let the database judge which instance group members usr will store
* fix: cut a derived username to the column width so a long local part can be provisioned
* chore: move the ee pin to the scim member doc fix
* chore: update ee-repo-ref to 0780955effb657807d14f0eb503cba1d49cee007
This commit updates the EE repository reference after PR #801 was merged in windmill-ee-private.
Previous ee-repo-ref: ee6452d489563204a98df883703f78d5e74cdd69
New ee-repo-ref: 0780955effb657807d14f0eb503cba1d49cee007
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(ai-sessions): share session artifacts with the workspace by link
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pyjp67oR269QAx3b4yf4oH
* chore: cache the shared artifact queries for offline sqlx
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pyjp67oR269QAx3b4yf4oH
* fix: replace a literal NUL byte in the shared artifact body limit comment
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pyjp67oR269QAx3b4yf4oH
* test: pin that a shared artifact is confined to its workspace's path
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pyjp67oR269QAx3b4yf4oH
* fix: sanitize shared artifact markdown and validate the artifact id on every route
The shared page renders another member's markdown, so ArtifactBody now runs the repo's rehype-raw + rehype-sanitize chain with the chat's link renderer on top; only the session viewer opts into the chat code block (mermaid, apply button). The link renderer keeps a link's text when its href is empty or unsafe, and the scheme check moves to a tested helper. The status route checks artifact_id like share does, so a NUL is a 400 rather than a 500.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pyjp67oR269QAx3b4yf4oH
* fix(ai-sessions): say which way re-sharing moves an artifact link
The popover offered "Update to v1" when a v2 link was open on a pinned v1, which reads as if v1 were newer. Each direction now has its own sentence and action: a newer version on screen updates the link, an older one shares that version instead, a rename updates the name.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: windmill-chat sdk for chat-mode flows in external frontends and raw apps
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aQiZNAU8g17kWkyTryS5J
* fix: keep streamed answers until persisted, finish turns after history fallback
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aQiZNAU8g17kWkyTryS5J
* feat: ai sdk transport and assistant-ui runtime for windmill-chat
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix: finish a turn from the flow result until its answer row lands, hash chat ids without crypto.subtle
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix: judge a turn answered by a persisted assistant row, wherever it was fetched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix: attribute a turn's answer to its own jobs, keep a local turn when switching conversations
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix: mirror local history on every change, attribute failure-handler answers to the turn
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix: new chat per token string in the React hook, idle after destroy, no reorder on view
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix: recreate the hook's chat on any credential change, namespace local history per user
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix: send the latest inputs from the React hook
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* chore: pin the EE ref that stamps trace context on exported log records
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WfgEm5rNRDyw4WUYf9ToVz
* chore: pin the EE ref with the sampling-decision test
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WfgEm5rNRDyw4WUYf9ToVz
* chore: update ee-repo-ref to 04a9f1efb4a52c79fcd20258b34780c86103d27f
This commit updates the EE repository reference after PR #800 was merged in windmill-ee-private.
Previous ee-repo-ref: d48361d66580618cb7a934d3c5f56a0c7e39ffaa
New ee-repo-ref: 04a9f1efb4a52c79fcd20258b34780c86103d27f
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(cli): list, get and restore trashed items from the CLI
* docs(cli): tell agents a sync push deletion is restorable with wmill trash
* refactor(cli): share the ApiError formatting and type trash flags as integers
test_run_step was the last run tool still starting a job on whatever the
model sent. Route it through runThroughForm, as test_run_script,
run_script and test_run_flow already are.
A step's arguments are its own, not the flow's: it is normally fed by its
input transforms, so the form is built from the step's target rather than
the flow's schema. loadSchemaFromModule resolves script and subflow steps
against the deployed version, which would offer the fields of code this
path is not about to run, so the schema comes from the same read the job
uses — inferred from a rawscript body, the draft script's content, or the
subflow's own schema.
The preprocessor's _ENTRYPOINT_OVERRIDE is declared by no schema, so it is
added inside the resolved startJob: proposed into the form instead, the
argument conforming would drop it and the preprocessor would silently run
its main. Its schema is inferred rather than read off the target for the
same reason a stored one cannot describe it: a schema speaks for the one
entrypoint it was inferred from.
executeFlowStepTestRun splits into resolveFlowStepRun plus a thin wrapper,
so the flow editor's own test_run_step keeps its behaviour but for one fix
it inherits: a deployed subflow step now runs with skipPreprocessor. The
flow editor's step test passes it too, and a parent flow pushes a subflow
step the same way (apply_preprocessor: false) — a preprocessor would take
the subflow's own inputs for a trigger event.
Claude-Session: https://claude.ai/code/session_018PBB2gw8FK4YmGPxu5Drbn
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cli): keep variables and resources a sync push repo never tracked
`wmill sync push` archives a script it no longer finds locally, but hard-deletes
a variable or a resource: the credentials go for good. A remote-only one is
equally a deletion being deployed and one the repository never had, provisioned
on the instance or written by a script at runtime, and reading the second as a
deletion is unrecoverable.
Committed history tells them apart. A push whose changeset deletes a variable or
resource now asks what this branch has ever tracked at `*.variable.*` /
`*.resource.*`; anything it has never recorded is kept on the remote (prompted
for on a TTY), and a real deletion, recorded before the commit that removed it,
still applies. Where the history cannot be read (shallow clone, sparse checkout,
no repository) there is no evidence either way, so the deletion stands as before
with a warning naming the remedy — the git-sync "Pull from repo" job runs in a
depth-1 clone and must keep deploying the deletions it always has.
`--delete-untracked-secrets` / `deleteUntrackedSecrets` opts a mirror-semantics
pipeline back into deleting them unattended.
Fixes GIT-980
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): classify secret-bearing deletions the way the push itself does
Three ways the suffix match missed:
- A fileset child can be any file, `inner.resource.yaml` included, and its
deletion re-pushes the parent rather than deleting anything. Classifying with
the push's own `getTypeStrFromPath`, behind the same fileset exclusion the
apply loop uses, keeps the two in step.
- Deleting `f/x.resource.file.ini` deletes the resource `f/x` outright, so
without that file in the pathspecs every file resource walked past the check.
Its two files now count as the one resource they delete.
- A `specificItems` item is committed as `y.<workspace>.variable.yaml` while
`elementsToMap` collapses it to the base path the changeset carries, so a
deletion the user did commit read as never tracked. The history is searched
under both names.
`gitRecordedPaths` also reads its history with `core.quotePath=false`: a path
with a non-ASCII byte came back C-quoted and matched nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): judge held-back deletions per object, not per file
`DELETE /variables/delete` takes the resource at the same path down with it, and
`DELETE /resources/delete` does the same to the variables its value references,
so a tracked deletion could destroy an untracked object the push had just
reported it was keeping. A file resource had the same shape from the other end:
two files for one resource, either survivor deleting it.
The unit is the server-side object. One file left unaccounted for by history now
holds the whole object back, so nothing in a group reported as kept is deleted.
The residual is a resource whose value references a variable at another path,
which stays possible and is called out in the PR.
Also corrects what the messages claim. Deleting a variable or resource is not
irrecoverable: both move to the workspace trash, which keeps them for three days
(migrations/20260326000000_trashbin.up.sql, CE since v1.665.0). The asymmetry
with a script is real but narrower, and the prompt defaults to No, so it should
say what it actually costs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): warn when sync push deletes variables the repo never tracked
`sync push` deletes a remote variable or resource that has no local file. An
object the repository has never tracked was provisioned outside it, by hand or
by a script at runtime, rather than deleted from it, and the change list said
nothing to tell the two apart.
The push still deploys every deletion — that is what the repo-is-the-mirror
contract means, and a shallow clone (the git-sync "Pull from repo" job, a
default actions/checkout) could not tell them apart anyway. What changes is that
the preview names the ones this branch's history has no record of, before the
prompt that confirms them, and points at the excludes that stop them recurring.
Deleting is also not final, which the CLI was alone in not saying: both handlers
move the item to the workspace trash first, restorable for three days
(migrations/20260326000000_trashbin.up.sql, CE since v1.665.0). A push that
deleted any now says so.
This replaces the earlier hold-back design. Keeping objects back changed what a
push deploys, needed a flag and a wmill.yaml key to opt out of, and could claim
to keep an object that a linked deletion then cascaded onto. Reporting cannot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): name the paths the untracked-deletion warning is about
A push can delete a tracked and a never-tracked resource together, where "1
resource" identified neither. The warning lists the paths instead of counting
them, so the reader knows which one to exclude.
Outside a git checkout it no longer opens "This branch's history", which
contradicted the reason it went on to give, and it drops the pronouns that
disagreed with a plural count. The history walk is skipped under --json-output,
where both notices are silenced and its result had no reader.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): vouch for an object with any file in history, not just deleted ones
The tracked set was built from the deletions being judged, so a companion file
the push was not deleting could not vouch for its object: a file resource whose
`.resource.yaml` stays while its content file goes was reported as never
tracked, though the repository plainly owned it. It is built from the whole
history now, which also turns the workspace-specific lookup around — history is
normalized to base paths, the form the changeset already carries, instead of
each candidate being searched for under two names.
The warning also prints one line per server-side object rather than per file, so
a file resource is the one deletion it is rather than two.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): name both kinds at a shared path, gate the history path conversion
Three from review:
A variable and a resource at one path are judged together, since deleting either
takes both, but they are two objects to name — keying the printed lines by path
alone dropped one of them.
`fromWorkspaceSpecificPath` strips a `.<workspace>` segment wherever it finds
one, so a history entry that merely looks workspace-suffixed was re-keyed onto a
different object, whose history then vouched for it. Only a path `specificItems`
claims is converted now. Not reachable from a server object (the backend rejects
`.` in paths), but history holds whatever was committed.
`secretBearingKey` lost its last caller when the tracked set moved to object
paths; removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): identify a secret-bearing object by kind as well as path
A variable and a resource can share a path and are still two backend objects:
`DELETE /variables/delete` drops the same-path resource unconditionally, while
`DELETE /resources/delete` drops only the variables its value references. Keying
tracked history by path alone let a committed variable vouch for a resource the
repository never had, which then went unmentioned. The cascade is a reason to
report both, not to treat them as one. A file resource's two files keep one id.
`git log HEAD` also fails on a repository with no commits, which was reported as
"its history could not be read. Check that git runs correctly in this directory"
— true of neither.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(cli): tighten the comments on the untracked-deletion warning
Halves the prose without dropping a constraint: the trashbin retention is
stated where the message says it rather than twice more in doc comments, and the
two stacked comments at the print site had come to contradict each other, one
still describing a same-path variable and resource as judged together.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): say where a deleted variable or resource went
`sync push` deletes a remote variable or resource that has no local file, and
said nothing more. Both handlers move the item to the workspace trash first,
restorable for three days (migrations/20260326000000_trashbin.up.sql, CE since
v1.665.0), and the CLI was the one surface never to mention it — the report
behind this concluded the deletion was final and there was nothing to restore.
A push that deleted any now ends with where they went and how long they have.
Drops the untracked-deletion warning this branch carried: distinguishing a
deletion the repo deployed from an object it never owned needs the branch's git
history, and roughly 130 lines to read it and be right about the answer, for a
claim the trash already softens. Two fixes it turned up in the data-table
migration guard, which reads history the same way, are kept: a path with a
non-ASCII byte came back C-quoted and matched nothing, and a repository with no
commits was reported as one where git does not run.
Fixes GIT-980
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): name the trashbin correctly and say who can restore
The tab is labelled Trashbin, not Trash, and `restore_trash_item` requires
admin, so a non-admin reading the old line would go looking for a control they
do not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(cli): move the migration-guard git fixes to their own PR
They fix `gitRecordedDatatableMigrationPaths`, which this PR no longer touches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): don't count a .lock deletion the push skips
The apply loop `continue`s past a non-raw-app, non-dbt `.lock` deletion before
reaching the delete switch, so nothing happens on the server. The classifier did
not mirror that, and `f/x.resource.file.lock` reaches it as a resource through
`isFileResource` — a resource type whose format_extension is literally `lock`
would have the notice announce a deletion the push never performed.
A raw-app or dbt `.lock`, the two that loop does not skip, classifies as its
bundle's own kind well before the file-resource check, so a suffix test is
enough.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(cli): state what the classification tests protect
The header described the change rather than the invariant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(flows): stop re-evaluating skip_if once a loop is in progress
skip_if is a one-time entry gate, but the flow stays at the same step
for a loop's whole lifetime, so it gets re-evaluated on every
iteration. previous_id stays pinned to the module preceding the loop,
but once the loop is InProgress the last completed job is an inner
iteration, and the results proxy in windmill-jseval aliases
results.<previous_id> to that job's result. skip_if then reads the
wrong value and can flip the loop's module to skipped after one
iteration.
Skip the check once status_module is already InProgress.
* fix(flows): match skip_if gate to sibling entry-state allowlists
Rewrite the skip_if gate as a positive allowlist (WaitingForPriorSteps
| WaitingForEvents | WaitingForExecutor), matching the shape already
used by the BranchOne/BranchAll predicate gates, instead of a negative
filter on InProgress. Restart-at-iteration also enters as InProgress;
document it as a separate case rather than folding it into the
aliasing reason, which does not apply there.
Add a regression test pinning skip_if to run once at while-loop entry.
* fix(ai-chat): hide other users' MCP servers from the chat unless shared
An admin's database role lets the resource listing return every user's
u/ MCP resource, so the chat's "+" menu and the assistant settings tab
offered servers that carry someone else's credentials. Both lists, and
the tool loader behind them, now keep a u/ server only when it belongs
to the current user or its extra_perms name them or one of their
groups. The Resources page is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0131DsyTiBR6Q54qPXbTX5sC
* fix(ai-chat): resolve the MCP viewer for the workspace being listed
Username and groups are per workspace, and a session chat can operate on
a workspace other than the one being browsed, so the filter now takes its
identity from that workspace's whoami rather than from userStore.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0131DsyTiBR6Q54qPXbTX5sC
* fix(ai-chat): list the whole MCP catalog before filtering, one predicate
The visibility filter runs after the server's LIMIT, so a 100-row page
could drop the viewer's own servers behind foreign u/ rows. The three
call sites now ask for 1000 and call the tested predicate directly.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0131DsyTiBR6Q54qPXbTX5sC
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>