mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 00:03:08 +00:00
fix(sessions): stop fork-create retry loop on first user message
Removed the SessionWrapper $effect that retroactively committed the session's workspace from the in-memory chat history. When opening a session whose previous commit attempt had failed (or whose response was lost) the effect ran in a tight retry loop, flooding the user with `workspace_pkey` violations from `create_workspace_fork`. The send path already commits through `AIChatManager.beforeSend` → `commitSessionWorkspace`, which is the deterministic moment-of-action. The $effect was a redundant reactive bridge that turned every backend failure into an infinite retry. Also hardens `materializeFork`/`commitSessionWorkspace` so the most common cause of the duplicate-key error self-heals: - `materializeFork` short-circuits when `fork.id` is already in `$userWorkspaces` (the previous create actually succeeded, we just lost the response). On a `workspace_pkey` catch, refresh the workspace list and adopt the existing row instead of toasting an error. - On a real `materializeFork` failure, `commitSessionWorkspace` now drops `pending_fork` so the session falls through to the workspace-pick fallback instead of looping on the same broken intent.
This commit is contained in:
@@ -31,7 +31,6 @@
|
||||
import SessionWorkspaceBar from './SessionWorkspaceBar.svelte'
|
||||
import SessionForkBar from './SessionForkBar.svelte'
|
||||
import {
|
||||
commitSessionWorkspace,
|
||||
createSession,
|
||||
getEffectiveWorkspaceId,
|
||||
moveSessionToNewFork,
|
||||
@@ -163,27 +162,16 @@
|
||||
}
|
||||
|
||||
// Workspace bar is shown only before the session sends its first user
|
||||
// message — after that the session's workspace is immutable.
|
||||
// message — after that the session's workspace is immutable. The
|
||||
// commit itself happens in `AIChatManager.beforeSend` (wired in
|
||||
// `createRuntime`) so it fires exactly once at send-time. A reactive
|
||||
// commit here would retry forever on backend failures (e.g. fork-id
|
||||
// collision after a previously-successful create whose response was
|
||||
// dropped) — restoration self-heal lives in `initRuntime` instead.
|
||||
const hasFirstUserMessage = $derived(
|
||||
runtime?.manager.displayMessages.some((m) => m.role === 'user') ?? false
|
||||
)
|
||||
|
||||
// Commit pending workspace pick (or current active workspace as
|
||||
// fallback) into `workspace_id` exactly once, when the first user
|
||||
// message lands. This is the only path that defines workspace_id.
|
||||
// When a pending fork is staged, this is also where the fork is
|
||||
// materialised via the API — no orphan forks for abandoned drafts.
|
||||
let committing = $state(false)
|
||||
$effect(() => {
|
||||
if (!session || !hasFirstUserMessage || session.workspace_id || committing) return
|
||||
committing = true
|
||||
commitSessionWorkspace(session.id, $workspaceStore ?? undefined)
|
||||
.catch((e) => console.error('Failed to commit session workspace', e))
|
||||
.finally(() => {
|
||||
committing = false
|
||||
})
|
||||
})
|
||||
|
||||
// Effective workspace for routing editor views — committed if set,
|
||||
// otherwise the pending pick, otherwise the current active workspace.
|
||||
const effectiveWorkspaceId = $derived(
|
||||
|
||||
@@ -308,7 +308,15 @@ export async function commitSessionWorkspace(
|
||||
if (s.pending_fork) {
|
||||
const fork = s.pending_fork
|
||||
const newId = await materializeFork(fork)
|
||||
if (!newId) return undefined
|
||||
if (!newId) {
|
||||
// Real failure (not a recovered duplicate). Drop the pending
|
||||
// fork so the session falls through to the workspace-pick
|
||||
// fallback on the next call and the unavailable-banner UX
|
||||
// can take over instead of looping on the same broken intent.
|
||||
s.pending_fork = undefined
|
||||
persistSessions()
|
||||
return undefined
|
||||
}
|
||||
if (get(workspaceStore) !== newId) switchWorkspace(newId)
|
||||
s.workspace_id = newId
|
||||
s.pending_fork = undefined
|
||||
@@ -360,7 +368,14 @@ export function renameSession(id: string, newSummary: string) {
|
||||
// path (commitSessionWorkspace) and the move-session-to-a-new-fork path
|
||||
// in the unavailable-session banner. Returns undefined on failure (a
|
||||
// user-facing toast is already emitted).
|
||||
//
|
||||
// Self-heal: if `fork.id` is already present in the user-workspaces
|
||||
// store, the previous create succeeded (whose response we apparently
|
||||
// lost). Adopt it silently instead of re-POSTing — the API would
|
||||
// otherwise reject with workspace_pkey. Likewise, if the API returns a
|
||||
// duplicate-key error we refresh the store and adopt the existing row.
|
||||
export async function materializeFork(fork: PendingFork): Promise<string | undefined> {
|
||||
if (get(userWorkspaces).some((w) => w.id === fork.id)) return fork.id
|
||||
try {
|
||||
await WorkspaceService.createWorkspaceFork({
|
||||
workspace: fork.parent_workspace_id,
|
||||
@@ -370,7 +385,12 @@ export async function materializeFork(fork: PendingFork): Promise<string | undef
|
||||
sendUserToast(`Created fork ${fork.name}`)
|
||||
return fork.id
|
||||
} catch (e: any) {
|
||||
sendUserToast(`Could not create fork: ${e?.body ?? e?.message ?? e}`, true)
|
||||
const msg = String(e?.body ?? e?.message ?? e)
|
||||
if (/workspace_pkey|duplicate key/i.test(msg)) {
|
||||
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
||||
if (get(userWorkspaces).some((w) => w.id === fork.id)) return fork.id
|
||||
}
|
||||
sendUserToast(`Could not create fork: ${msg}`, true)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user