mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-24 00:04:14 +00:00
* studio: keep each chat's sampling params and system prompt with the chat The composer pills, permission level and retrieval controls already travel with the thread. The sampling params and the system prompt did not, so returning to a chat started under one prompt showed whichever prompt the last chat had, and every chat shared one set of sliders. Those eight join the snapshot: temperature, top P, top K, min P, the two penalties, the system prompt and its variables. They are the only thread-scoped settings that live under params rather than as store fields of their own, so the read and apply paths route them through there and the whole snapshot is still one object on the thread row. An edit made with a chat open lands on that chat; with no chat open it moves the installation defaults, as before. A model's own recommendation is not an edit: both paths that apply one now say so, or loading a model while a chat was open would pin that chat to the model's sampling. Context and checkpoint stay out. They describe the model that loaded rather than the conversation, and a chat restoring a budget the current model cannot hold is the failure that exclusion exists to prevent. * studio: keep a chat's sampling through a model load and a held edit Three follow-ups from review on the per-chat sampling change. A model's recommendation still landed in the live params: fromModelDefaults only moved where the value was persisted. A chat with stored sampling ran the model's instead, and the next unrelated edit snapshotted that over what the chat had. setParams now puts back the keys the chat holds; one it never set still follows the model. The Qwen mode table is the model's recommendation, not a value the user picked, so both paths that apply it are marked. Loading the model and toggling Think reach the same table, so marking one and not the other pinned the chat by the other route. The held-edit flush read the sampling keys as store fields. They sit under params, so it read undefined, the sanitizer dropped them, and an edit made while the chat's snapshot was in flight was lost when the user navigated away. * studio: persist a model's sampling defaults even when the chat pins them Restoring the chat's values before the diff made every pinned key equal on both sides, so it dropped out of changedParams and never reached the installation defaults. One chat pinning temperature left every later new chat on whatever model loaded before it. The restore is a live-store concern only. Persistence diffs the model's own object again, as it did before the restore existed, so the recommendation reaches the defaults while the open chat keeps what it stored. getChangedInferenceParams bumps the mutation versions, so it stays a single call rather than one diff per purpose. * studio: keep a chat's sampling across a model switch, both directions setCheckpoint replays the destination model's remembered params itself, without going through setParams, so the restore never ran there. An external switch has no load after it to correct the result, so the chat adopted that model's prompt and sampling and kept them. The restore now runs on that path too, and persistence still reads the unrestored object so the model's own values reach the installation defaults. The other direction leaked the same way. rememberOutgoingModel snapshots the live params, which inside a chat are that chat's, and on a model with no entry it persists that snapshot whole. Filtering the incoming edit cannot undo a write that already happened, so the outgoing snapshot is now built without the values the open chat owns. What the model already remembered wins over the installation copy, so stepping off a model inside a chat does not flatten a preference the model was given outside one. persistReplayedParams writes the installation defaults on the setCheckpoint path, so it keeps the in-memory copy level the same way setParams does. * Keep a chat's sampling edits through hydration, model loads and remembered-params writes * Keep a held chat edit out of the outgoing model, and keep a default published mid-pairing withoutActiveThreadParams returned early while a chat's read was still out, so switching model in that window snapshotted the open chat's temperature and system prompt into the outgoing model's shared memory. The sibling restore path already read the held value first; this one consulted only the override. Driving the real store put a chat's own sentinel straight into paramsByModel, which the simulation harness already treats as a leak. Second, a model default published inside the pairing window was sent to the settings route but then rolled back in memory from the pre-window sample, so the session and the server disagreed and the stale value was pinned onto the next chat with no snapshot. noteThreadScopedDefaults also returned early when nothing had been applied yet, which is the common case, so the value had nowhere to land. * Put the installation defaults back when a chat is left mid-read Leaving a chat while its settings read is still out writes the edits made in it to that chat's row, but the store kept showing them, so the next chat took them: a snapshot-less chat captured the store as the installation defaults and was pinned with the previous chat's temperature and system prompt. Restore the pre-window values over the committed fields, and only when the chat is actually being left, so a retry after a failed read and a fork both leave the edit on screen. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: tighten the per-chat inference comments Comments only. Collapses the multi-line explanations in the thread-scoped sampling path to their shortest clear form and drops the restatements, keeping the reasons behind the pairing window, the held-edit capture and the outgoing-model filter. --------- Co-authored-by: Unsloth <michaelhan@Michaels-MacBook-Pro.local> Co-authored-by: Daniel Han <danielhanchen@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
55 lines
2.2 KiB
JavaScript
55 lines
2.2 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
// store-settings-resolver, plus the two things a per-chat sampling simulation needs.
|
|
//
|
|
// 1. The thread row: the store reaches it through a dynamic relative import the "@/" stub
|
|
// map does not cover, so it has to be intercepted here.
|
|
// 2. Scenario scoping: the store keeps the whole feature in module state, so a simulation
|
|
// running hundreds of orderings needs a fresh copy per ordering. A "?scenario=N" query
|
|
// gives one, and the modules listed below are re-instantiated with it so they talk to
|
|
// THAT copy. Everything else stays shared, which is what makes the stubs observable.
|
|
import { existsSync } from "node:fs";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
import { resolve as resolveSettings } from "./store-settings-resolver.mjs";
|
|
|
|
const ROW_STUB = new URL(
|
|
"./helpers/store-stubs/chat-history-storage.ts",
|
|
import.meta.url,
|
|
).href;
|
|
|
|
/** Modules that must follow the scenario's store instance, not the shared one. */
|
|
const SCENARIO_SCOPED = new Set([
|
|
new URL("../src/features/chat/stores/chat-runtime-store.ts", import.meta.url)
|
|
.href,
|
|
new URL("../src/features/chat/utils/qwen-params.ts", import.meta.url).href,
|
|
]);
|
|
|
|
function firstExisting(base) {
|
|
for (const candidate of [`${base}.ts`, `${base}/index.ts`, base]) {
|
|
if (existsSync(candidate)) {
|
|
return pathToFileURL(candidate).href;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function resolve(specifier, context, next) {
|
|
if (specifier.endsWith("utils/chat-history-storage")) {
|
|
return next(ROW_STUB, context);
|
|
}
|
|
const scenario = context.parentURL?.startsWith("file:")
|
|
? new URL(context.parentURL).searchParams.get("scenario")
|
|
: null;
|
|
if (scenario !== null && specifier.startsWith(".")) {
|
|
// Relative resolution drops the parent's query, so this is the plain path.
|
|
const target = firstExisting(
|
|
fileURLToPath(new URL(specifier, context.parentURL)),
|
|
);
|
|
if (target !== null && SCENARIO_SCOPED.has(target)) {
|
|
return next(`${target}?scenario=${scenario}`, context);
|
|
}
|
|
}
|
|
return resolveSettings(specifier, context, next);
|
|
}
|