Harden Exact reconcile and soften post-Generate warm.

Overwrite foreign steps/cfg in sparse patches when the user did not ask for params, and skip the cold-load path when Ollama already has the chat model resident.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-23 06:58:53 +03:00
co-authored by Cursor
parent 5f33130381
commit 7aadb40a11
7 changed files with 117 additions and 35 deletions
+34 -11
View File
@@ -667,6 +667,12 @@
const clearSessionKeys = []; const clearSessionKeys = [];
for (const key of EXACT_GENERATE_PARAM_KEYS) { for (const key of EXACT_GENERATE_PARAM_KEYS) {
if (out[key] != null) { if (out[key] != null) {
if (!userParamIntent && defaults[key] != null && String(out[key]) !== String(defaults[key])) {
out[key] = defaults[key];
if (sessionExact?.[key] != null && String(sessionExact[key]) !== String(defaults[key])) {
clearSessionKeys.push(key);
}
}
continue; continue;
} }
if (userParamIntent && sessionExact?.[key] != null) { if (userParamIntent && sessionExact?.[key] != null) {
@@ -1140,7 +1146,7 @@
continue; continue;
} }
const eps = key === "steps" ? 0.5 : 0.051; const eps = key === "steps" ? 0.5 : 0.051;
if (patch && patch[key] != null && !numClose(patch[key], want, eps)) { if (userParamIntent && patch && patch[key] != null && !numClose(patch[key], want, eps)) {
continue; continue;
} }
if (userParamIntent && sessionExact && sessionExact[key] != null) { if (userParamIntent && sessionExact && sessionExact[key] != null) {
@@ -2361,7 +2367,7 @@ ${patch.prompt}`;
if (want == null) { if (want == null) {
return false; return false;
} }
if (patch?.[k] != null && String(patch[k]) !== String(want)) { if (userIntent && patch?.[k] != null && String(patch[k]) !== String(want)) {
return false; return false;
} }
if (userIntent && state.sessionExact?.[k] != null) { if (userIntent && state.sessionExact?.[k] != null) {
@@ -5496,16 +5502,16 @@ ${patch.prompt}`;
return new Promise((resolve) => { return new Promise((resolve) => {
const model = $2("sa_model")?.value; const model = $2("sa_model")?.value;
if (!model || typeof genericRequest !== "function") { if (!model || typeof genericRequest !== "function") {
resolve(false); resolve({ ok: false, alreadyResident: false });
return; return;
} }
if (!force && !state.llmParked) { if (!force && !state.llmParked) {
resolve(false); resolve({ ok: false, alreadyResident: false });
return; return;
} }
const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434"; const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434";
let settled = false; let settled = false;
const finish = (ok) => { const finish = (ok, alreadyResident = false) => {
if (settled) { if (settled) {
return; return;
} }
@@ -5514,10 +5520,19 @@ ${patch.prompt}`;
if (ok) { if (ok) {
state.expectColdLoad = false; state.expectColdLoad = false;
} }
resolve(!!ok); resolve({ ok: !!ok, alreadyResident: !!alreadyResident });
}; };
setTimeout(() => finish(false), 18e4); setTimeout(() => finish(false), 18e4);
genericRequest("AssistentWarmLlm", { baseUrl, model }, () => finish(true), 0, () => finish(false)); genericRequest(
"AssistentWarmLlm",
{ baseUrl, model },
(data) => {
const skipped = !!(data && (data.skipped === "already_resident" || data.already_resident === true));
finish(true, skipped);
},
0,
() => finish(false)
);
}); });
} }
function cancelWaitForNewImage() { function cancelWaitForNewImage() {
@@ -5845,7 +5860,8 @@ ${patch.prompt}`;
} }
state.generating = false; state.generating = false;
setInterruptVisible(state.busy); setInterruptVisible(state.busy);
state.expectColdLoad = true; const parkedBeforeWarm = !!state.llmParked;
state.expectColdLoad = parkedBeforeWarm;
if (jobs && lastSrc && state.selectedGenResultId && epoch === state.chatEpoch) { if (jobs && lastSrc && state.selectedGenResultId && epoch === state.chatEpoch) {
const row = state.genResults.find((r) => r.id === state.selectedGenResultId); const row = state.genResults.find((r) => r.id === state.selectedGenResultId);
if (row?.patch) { if (row?.patch) {
@@ -5857,9 +5873,16 @@ ${patch.prompt}`;
const multiDone = !!(jobs && finishedGenResultCount() > 1); const multiDone = !!(jobs && finishedGenResultCount() > 1);
const willAutoCritique = !multiDone && !!$2("sa_auto_critique")?.checked; const willAutoCritique = !multiDone && !!$2("sa_auto_critique")?.checked;
if (state.view === "chat" && paneVisible && !willAutoCritique && epoch === state.chatEpoch) { if (state.view === "chat" && paneVisible && !willAutoCritique && epoch === state.chatEpoch) {
startBusyUi("warming"); if (parkedBeforeWarm || state.expectColdLoad) {
setStatus("\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E LLM \u0432 GPU\u2026"); startBusyUi("warming");
await warmLlm({ force: true }); setStatus("\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E LLM \u0432 GPU\u2026");
}
const warmResult = await warmLlm({ force: true });
if (warmResult?.alreadyResident) {
state.expectColdLoad = false;
} else if (!warmResult?.ok) {
state.expectColdLoad = true;
}
} }
if (epoch !== state.chatEpoch) { if (epoch !== state.chatEpoch) {
return null; return null;
+1 -3
View File
@@ -4,9 +4,7 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat +
**Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate that blocks new user sends. What a reply does to generation state is decided once, in `resolveTurnIntent`: the model's `actions:["generate"]` / `look_at` win, RU intent heuristics only back it up when the model forgets, and an explicit «запомни, не генерируй» vetoes both. **Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate that blocks new user sends. What a reply does to generation state is decided once, in `resolveTurnIntent`: the model's `actions:["generate"]` / `look_at` win, RU intent heuristics only back it up when the model forgets, and an explicit «запомни, не генерируй» vetoes both.
**Version 0.14.1**On Generate, force Exact turbo/raw params when live Swarm still has foreign leftovers (e.g. steps 20 / cfg 7). `detectKreaProfileName` no longer invents turbo for unlabeled ckpts (realismByStableYogi → raw). Live context injects `krea_profile` + `recommended_params`. RU «шаг/шагами» counts as param intent. Soft sparse-prompt exception when session ≠ Exact profile. **Version 0.14.1**Force Exact turbo/raw numbers on Generate when live Swarm still has foreign leftovers (e.g. steps 20 / cfg 7), including when the profile *label* is turbo/raw. detectKreaProfileName no longer invents turbo for unlabeled ckpts (realismByStableYogi → raw). Live context injects krea_profile + ecommended_params. RU «шаг/шагами» counts as param intent. Soft sparse-prompt exception when session ≠ Exact profile. Post-Generate warm still runs with park off, but skips the cold path when Ollama already has the chat model resident.
**Version 0.14.1** — Generate always merges Exact turbo|raw `steps`/`cfg`/`sigma_shift` (client-authoritative; sparse LLM omit is safe). Park/warm skip no-op Ollama round-trips when the chat model is already (un)loaded via `/api/ps`. Builds on 0.14.0.
**Version 0.14.0****Чат = сессия генерации**: у каждого чата свои params/LoRA/checkpoint/кадр/refs; модель шлёт sparse-дельту + `generate`/`look_at`/`ask`; без вкладки Карточки и Civitai/wanted hops. **Сжатие контекста**: rolling-саммари той же Ollama-моделью, чип бюджета `N / num_ctx`, авто перед отправкой, `/compress`. **Version 0.14.0****Чат = сессия генерации**: у каждого чата свои params/LoRA/checkpoint/кадр/refs; модель шлёт sparse-дельту + `generate`/`look_at`/`ask`; без вкладки Карточки и Civitai/wanted hops. **Сжатие контекста**: rolling-саммари той же Ollama-моделью, чип бюджета `N / num_ctx`, авто перед отправкой, `/compress`.
+34 -10
View File
@@ -1353,7 +1353,7 @@
if (want == null) { if (want == null) {
return false; return false;
} }
if (patch?.[k] != null && String(patch[k]) !== String(want)) { if (userIntent && patch?.[k] != null && String(patch[k]) !== String(want)) {
return false; return false;
} }
if (userIntent && state.sessionExact?.[k] != null) { if (userIntent && state.sessionExact?.[k] != null) {
@@ -4751,20 +4751,25 @@
} }
/** Re-load chat model into VRAM. force=true after Generate even without park — Krea often evicts Ollama. */ /** Re-load chat model into VRAM. force=true after Generate even without park — Krea often evicts Ollama. */
/**
* Re-load chat model into VRAM. force=true after Generate even without park
* Krea often evicts Ollama. Soft: AssistentWarmLlm skipped=already_resident
* clears expectColdLoad without a fake cold path. Resolves { ok, alreadyResident }.
*/
function warmLlm({ force = false } = {}) { function warmLlm({ force = false } = {}) {
return new Promise((resolve) => { return new Promise((resolve) => {
const model = $('sa_model')?.value; const model = $('sa_model')?.value;
if (!model || typeof genericRequest !== 'function') { if (!model || typeof genericRequest !== 'function') {
resolve(false); resolve({ ok: false, alreadyResident: false });
return; return;
} }
if (!force && !state.llmParked) { if (!force && !state.llmParked) {
resolve(false); resolve({ ok: false, alreadyResident: false });
return; return;
} }
const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434'; const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
let settled = false; let settled = false;
const finish = (ok) => { const finish = (ok, alreadyResident = false) => {
if (settled) { if (settled) {
return; return;
} }
@@ -4773,11 +4778,20 @@
if (ok) { if (ok) {
state.expectColdLoad = false; state.expectColdLoad = false;
} }
resolve(!!ok); resolve({ ok: !!ok, alreadyResident: !!alreadyResident });
}; };
// VL cold-load can exceed a minute — don't time out the flag early. // VL cold-load can exceed a minute — don't time out the flag early.
setTimeout(() => finish(false), 180000); setTimeout(() => finish(false), 180000);
genericRequest('AssistentWarmLlm', { baseUrl, model }, () => finish(true), 0, () => finish(false)); genericRequest(
'AssistentWarmLlm',
{ baseUrl, model },
(data) => {
const skipped = !!(data && (data.skipped === 'already_resident' || data.already_resident === true));
finish(true, skipped);
},
0,
() => finish(false),
);
}); });
} }
@@ -5130,7 +5144,10 @@
state.generating = false; state.generating = false;
setInterruptVisible(state.busy); setInterruptVisible(state.busy);
state.expectColdLoad = true; // Soft cold flag: only assume eviction when we parked. Otherwise wait for
// warmLlm — already_resident means keep chatting without a cold path.
const parkedBeforeWarm = !!state.llmParked;
state.expectColdLoad = parkedBeforeWarm;
if (jobs && lastSrc && state.selectedGenResultId && epoch === state.chatEpoch) { if (jobs && lastSrc && state.selectedGenResultId && epoch === state.chatEpoch) {
const row = state.genResults.find((r) => r.id === state.selectedGenResultId); const row = state.genResults.find((r) => r.id === state.selectedGenResultId);
@@ -5144,9 +5161,16 @@
const multiDone = !!(jobs && finishedGenResultCount() > 1); const multiDone = !!(jobs && finishedGenResultCount() > 1);
const willAutoCritique = !multiDone && !!$('sa_auto_critique')?.checked; const willAutoCritique = !multiDone && !!$('sa_auto_critique')?.checked;
if (state.view === 'chat' && paneVisible && !willAutoCritique && epoch === state.chatEpoch) { if (state.view === 'chat' && paneVisible && !willAutoCritique && epoch === state.chatEpoch) {
startBusyUi('warming'); if (parkedBeforeWarm || state.expectColdLoad) {
setStatus('Возвращаю LLM в GPU…'); startBusyUi('warming');
await warmLlm({ force: true }); setStatus('Возвращаю LLM в GPU…');
}
const warmResult = await warmLlm({ force: true });
if (warmResult?.alreadyResident) {
state.expectColdLoad = false;
} else if (!warmResult?.ok) {
state.expectColdLoad = true;
}
} }
if (epoch !== state.chatEpoch) { if (epoch !== state.chatEpoch) {
+10 -4
View File
@@ -81,10 +81,16 @@ export function exactKeysToForce(live, defaults, {
if (want == null) { if (want == null) {
continue; continue;
} }
const eps = key === 'steps' ? 0.5 : 0.051; const eps = key === 'steps' ? 0.5 : 0.051;
// Patch asked for a non-Exact number — honor it. // Honor intentional non-Exact patch only when the user asked for params.
if (patch && patch[key] != null && !numClose(patch[key], want, eps)) { // Sparse LLM echoes of live 20/7 must not block Exact (turbo/raw label -> numbers).
continue; if (
userParamIntent
&& patch
&& patch[key] != null
&& !numClose(patch[key], want, eps)
) {
continue;
} }
if (userParamIntent && sessionExact && sessionExact[key] != null) { if (userParamIntent && sessionExact && sessionExact[key] != null) {
continue; continue;
+15 -4
View File
@@ -372,7 +372,7 @@ export function resolveExactProfileDefaults({ exact, profiles, profileName } = {
} }
/** /**
* On generate, inject Exact profile steps/cfg/sigma when the patch omitted them. * On generate, inject Exact profile steps/cfg/sigma when omitted, and overwrite foreign leftovers when the user did not ask for params.
* Client is authoritative so sparse LLM deltas are safe. Honors explicit patch values * Client is authoritative so sparse LLM deltas are safe. Honors explicit patch values
* and sessionExact when the user asked for params this turn. * and sessionExact when the user asked for params this turn.
* Returns clearSessionKeys so the UI apply path is not blocked by stale sessionExact. * Returns clearSessionKeys so the UI apply path is not blocked by stale sessionExact.
@@ -390,9 +390,20 @@ export function mergeExactParamsForGenerate(patch, {
const defaults = resolveExactProfileDefaults({ exact, profiles, profileName }); const defaults = resolveExactProfileDefaults({ exact, profiles, profileName });
const out = { ...patch }; const out = { ...patch };
const clearSessionKeys = []; const clearSessionKeys = [];
for (const key of EXACT_GENERATE_PARAM_KEYS) { for (const key of EXACT_GENERATE_PARAM_KEYS) {
if (out[key] != null) { if (out[key] != null) {
continue; // Sparse LLM may echo live leftovers (20/7). Without user intent, force Exact.
if (
!userParamIntent
&& defaults[key] != null
&& String(out[key]) !== String(defaults[key])
) {
out[key] = defaults[key];
if (sessionExact?.[key] != null && String(sessionExact[key]) !== String(defaults[key])) {
clearSessionKeys.push(key);
}
}
continue;
} }
if (userParamIntent && sessionExact?.[key] != null) { if (userParamIntent && sessionExact?.[key] != null) {
out[key] = sessionExact[key]; out[key] = sessionExact[key];
+9 -1
View File
@@ -68,7 +68,15 @@ describe('kreaProfile', () => {
); );
// Intentional non-Exact patch value is kept. // Intentional non-Exact patch value is kept.
assert.deepEqual( assert.deepEqual(
exactKeysToForce({ steps: 20, cfg: 7, sigma_shift: 1.15 }, turbo, { patch: { cfg: 7 } }), exactKeysToForce({ steps: 20, cfg: 7, sigma_shift: 1.15 }, turbo, { patch: { cfg: 7 } }),
['steps', 'cfg'],
);
assert.deepEqual(
exactKeysToForce(
{ steps: 20, cfg: 7, sigma_shift: 1.15 },
turbo,
{ patch: { cfg: 7 }, userParamIntent: true },
),
['steps'], ['steps'],
); );
assert.deepEqual( assert.deepEqual(
+14 -2
View File
@@ -123,10 +123,22 @@ describe('session.js', () => {
const explicit = mergeExactParamsForGenerate( const explicit = mergeExactParamsForGenerate(
{ generate: true, steps: 12, cfg: 2 }, { generate: true, steps: 12, cfg: 2 },
{ exact, profiles: exact.profiles, profileName: 'turbo' }, { exact, profiles: exact.profiles, profileName: 'turbo', userParamIntent: true },
); );
assert.equal(explicit.patch.steps, 12); assert.equal(explicit.patch.steps, 12);
assert.equal(explicit.patch.cfg, 2); assert.equal(explicit.patch.cfg, 2);
const forced = mergeExactParamsForGenerate(
{ generate: true, steps: 20, cfg: 7 },
{
exact: { generation: { steps: 8, cfg: 1 }, profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } } },
profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } },
profileName: 'turbo',
userParamIntent: false,
},
);
assert.equal(forced.patch.steps, 8);
assert.equal(forced.patch.cfg, 1);
}); });
it('resolveExactProfileDefaults picks raw over generation defaults', () => { it('resolveExactProfileDefaults picks raw over generation defaults', () => {