Let the install set how many steps a run may take
The two limits were compiled in: a task run stopped at AppData.HarnessMaxToolSteps (200) and a chat reply at AppData.BotReplyMaxToolSteps (8), so an install that needed a longer task had to rebuild. Both are now `app_settings` rows (`harness_max_tool_steps` and `bot_reply_max_tool_steps`), read at the two loop call sites through grabHarnessMaxToolSteps and grabReplyMaxToolSteps, and edited by the super admin on Settings > General beside the prompt appendix. Zero clears a limit back to the code default, like the bot file quota. readStepLimit keeps the saved number only when it is a whole number of at least 1 and no more than AppData.BotStepLimitMax (1000), so a blank, a zero or a typo falls back instead of stopping every run before its first turn, and a huge number still ends in the wrap-up report. runTextLoop keeps AppData.HarnessMaxToolSteps as its default, so it stays free of database reads and its tests run as they did.
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
TaskReminderNote,
|
||||
WorkspaceLearningNote,
|
||||
} from "@/src/data/harness-prompts";
|
||||
import { grabReplyMaxToolSteps } from "@/src/functions/backend/db/app-settings/grab-run-limits";
|
||||
import grabBotMessages from "@/src/functions/backend/db/bots/grab-bot-messages";
|
||||
import grabBotPrompt from "@/src/functions/backend/db/bots/grab-bot-prompt";
|
||||
import grabBotComputers from "@/src/functions/backend/db/computers/grab-bot-computers";
|
||||
@@ -173,7 +174,7 @@ export default async function runBotReply({
|
||||
}),
|
||||
}),
|
||||
tools: [...tools, ...file_tools, ...extensions.tools, ...skills.tools],
|
||||
max_steps: AppData.BotReplyMaxToolSteps,
|
||||
max_steps: await grabReplyMaxToolSteps(),
|
||||
bot_id: bot.id,
|
||||
workspace_id,
|
||||
signal,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TaskRunNote,
|
||||
WorkspaceLearningNote,
|
||||
} from "@/src/data/harness-prompts";
|
||||
import { grabHarnessMaxToolSteps } from "@/src/functions/backend/db/app-settings/grab-run-limits";
|
||||
import grabBotComputers from "@/src/functions/backend/db/computers/grab-bot-computers";
|
||||
import grabBotPrompt from "@/src/functions/backend/db/bots/grab-bot-prompt";
|
||||
import grabUserWorkspaces from "@/src/functions/backend/db/workspaces/grab-user-workspaces";
|
||||
@@ -227,6 +228,7 @@ export default async function runBotTask({
|
||||
},
|
||||
],
|
||||
tools: [...tools, ...extensions.tools, ...skills.tools],
|
||||
max_steps: await grabHarnessMaxToolSteps(),
|
||||
bot_id: bot.id,
|
||||
workspace_id,
|
||||
signal,
|
||||
|
||||
+13
-1
@@ -246,7 +246,10 @@ export const AppData = {
|
||||
HarnessCooldownMs: 60 * 1000,
|
||||
HarnessMaxCooldownMs: 24 * 60 * 60 * 1000,
|
||||
/**
|
||||
* Model turns in one text run before it must stop and report.
|
||||
* Model turns in one text run before it must stop and report. The
|
||||
* install may save its own limit (the `harness_max_tool_steps` setting,
|
||||
* Settings > General), which wins over this one; a task run reads it
|
||||
* through `grabHarnessMaxToolSteps`.
|
||||
*/
|
||||
HarnessMaxToolSteps: 200,
|
||||
/**
|
||||
@@ -389,8 +392,17 @@ export const AppData = {
|
||||
WorkspaceActivityItems: 6,
|
||||
/**
|
||||
* Tool turns a chat reply may take. Long work goes to a task instead.
|
||||
* The install may save its own limit (the `bot_reply_max_tool_steps`
|
||||
* setting, Settings > General), which wins over this one; a reply
|
||||
* reads it through `grabReplyMaxToolSteps`.
|
||||
*/
|
||||
BotReplyMaxToolSteps: 8,
|
||||
/**
|
||||
* The most steps a saved run limit may ask for, for a task run or a
|
||||
* chat reply. A typo in the settings page can't turn one run into an
|
||||
* hour of tool calls, and a huge limit still ends with a report.
|
||||
*/
|
||||
BotStepLimitMax: 1000,
|
||||
/**
|
||||
* Chat attachments: per file, per message, and how much of a
|
||||
* document's text a reply reads.
|
||||
|
||||
@@ -110,6 +110,12 @@ export const CoordinatorBotName = "Coordinator";
|
||||
*/
|
||||
export const BotSystemPromptAppendSettingKey = "bot_system_prompt_append";
|
||||
|
||||
/**
|
||||
* The `app_settings` key holding the install's own limit on the tool turns
|
||||
* one chat reply may take. Unset means `AppData.BotReplyMaxToolSteps`.
|
||||
*/
|
||||
export const BotReplyMaxToolStepsSettingKey = "bot_reply_max_tool_steps";
|
||||
|
||||
/**
|
||||
* A bot's face is its status. There are no badges or spinners.
|
||||
*/
|
||||
|
||||
@@ -53,6 +53,12 @@ export const Harnesses = [
|
||||
|
||||
export type HarnessKind = (typeof Harnesses)[number]["value"];
|
||||
|
||||
/**
|
||||
* The `app_settings` key holding the install's own limit on model turns in
|
||||
* one run (a task). Unset means `AppData.HarnessMaxToolSteps`.
|
||||
*/
|
||||
export const HarnessMaxToolStepsSettingKey = "harness_max_tool_steps";
|
||||
|
||||
/**
|
||||
* Vendors preferred for automatic picks, best first. Free models from any
|
||||
* other vendor come after these, and only if they look competent.
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import { BotReplyMaxToolStepsSettingKey } from "@/src/dict/bot-dict";
|
||||
import { HarnessMaxToolStepsSettingKey } from "@/src/dict/harness-dict";
|
||||
import readStepLimit from "@/src/utils/read-step-limit";
|
||||
import grabAppSetting from "./grab-app-setting";
|
||||
|
||||
/**
|
||||
* Model turns a task run may take: the install's own limit
|
||||
* (`harness_max_tool_steps`, Settings > General), else
|
||||
* `AppData.HarnessMaxToolSteps`. `runTextLoop`'s default is the code's one,
|
||||
* so a task run passes this in.
|
||||
*/
|
||||
export async function grabHarnessMaxToolSteps() {
|
||||
return readStepLimit({
|
||||
saved: await grabAppSetting({ key: HarnessMaxToolStepsSettingKey }),
|
||||
fallback: AppData.HarnessMaxToolSteps,
|
||||
max: AppData.BotStepLimitMax,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Model turns a chat reply may take: the install's own limit
|
||||
* (`bot_reply_max_tool_steps`, Settings > General), else
|
||||
* `AppData.BotReplyMaxToolSteps`.
|
||||
*/
|
||||
export async function grabReplyMaxToolSteps() {
|
||||
return readStepLimit({
|
||||
saved: await grabAppSetting({ key: BotReplyMaxToolStepsSettingKey }),
|
||||
fallback: AppData.BotReplyMaxToolSteps,
|
||||
max: AppData.BotStepLimitMax,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Both limits with their defaults, for the settings page.
|
||||
*/
|
||||
export default async function grabRunLimitsOverview() {
|
||||
const [task_steps, reply_steps] = await Promise.all([
|
||||
grabHarnessMaxToolSteps(),
|
||||
grabReplyMaxToolSteps(),
|
||||
]);
|
||||
|
||||
const overview: RunLimitsOverview = {
|
||||
task_steps,
|
||||
reply_steps,
|
||||
task_default: AppData.HarnessMaxToolSteps,
|
||||
reply_default: AppData.BotReplyMaxToolSteps,
|
||||
max: AppData.BotStepLimitMax,
|
||||
};
|
||||
|
||||
return overview;
|
||||
}
|
||||
|
||||
export type RunLimitsOverview = {
|
||||
/**
|
||||
* The limit a task run uses right now.
|
||||
*/
|
||||
task_steps: number;
|
||||
/**
|
||||
* The limit a chat reply uses right now.
|
||||
*/
|
||||
reply_steps: number;
|
||||
/**
|
||||
* What a task run uses when the install saved no limit.
|
||||
*/
|
||||
task_default: number;
|
||||
/**
|
||||
* What a chat reply uses when the install saved no limit.
|
||||
*/
|
||||
reply_default: number;
|
||||
/**
|
||||
* The most the settings page accepts.
|
||||
*/
|
||||
max: number;
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import sanitizeUserBio from "@/src/functions/backend/db/users/sanitize-user-bio"
|
||||
import grabUserPasswordHash from "@/src/functions/backend/auth/grab-user-password-hash";
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
import grabRunLimitsOverview from "@/src/functions/backend/db/app-settings/grab-run-limits";
|
||||
import grabStorageQuotaOverview from "@/src/functions/backend/db/app-settings/grab-storage-quota-overview";
|
||||
import { grabSystemPromptAppendOverview } from "@/src/functions/backend/db/app-settings/grab-system-prompt-append";
|
||||
import runningVersion from "@/src/utils/running-version";
|
||||
@@ -45,6 +46,7 @@ export default async function grabSettingsServerProps({ req }: Params) {
|
||||
has_password,
|
||||
storage_quota: await grabStorageQuotaOverview().catch(() => null),
|
||||
system_prompt_append: await grabSystemPromptAppendOverview().catch(() => null),
|
||||
run_limits: await grabRunLimitsOverview().catch(() => null),
|
||||
running_version: runningVersion({ env: process.env }),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useState } from "react";
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import Form from "@moduletrace/twui/form/Form";
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import type { RunLimitsOverview } from "@/src/functions/backend/db/app-settings/grab-run-limits";
|
||||
import SettingsSaveBar from "../../(partials)/settings-save-bar";
|
||||
|
||||
type Props = {
|
||||
limits?: RunLimitsOverview | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* How many model turns a task run and a chat reply may take, saved on its
|
||||
* own without a reload. Zero puts the code's default back for that one.
|
||||
*/
|
||||
export default function RunLimitsForm({ limits }: Props) {
|
||||
const [overview, setOverview] = useState(limits || undefined);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [task_steps, setTaskSteps] = useState(String(limits?.task_steps ?? ""));
|
||||
const [reply_steps, setReplySteps] = useState(String(limits?.reply_steps ?? ""));
|
||||
/**
|
||||
* The inputs only read `defaultValue` when they mount, so discarding or
|
||||
* saving remounts them with the saved numbers.
|
||||
*/
|
||||
const [form_key, setFormKey] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [status, setStatus] = useState<{
|
||||
msg?: string;
|
||||
error?: boolean;
|
||||
success?: boolean;
|
||||
}>();
|
||||
|
||||
const save = async () => {
|
||||
setLoading(true);
|
||||
setStatus(undefined);
|
||||
|
||||
const res = await fetchApi<{}, APIResponseObject<RunLimitsOverview>>(
|
||||
"/api/admin/settings/run-limits",
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
task_steps: Number(task_steps) || 0,
|
||||
reply_steps: Number(reply_steps) || 0,
|
||||
},
|
||||
},
|
||||
).catch(() => null);
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (!res?.success || !res.singleRes) {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: res?.msg || `Couldn't save the limits.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setOverview(res.singleRes);
|
||||
setTaskSteps(String(res.singleRes.task_steps));
|
||||
setReplySteps(String(res.singleRes.reply_steps));
|
||||
setFormKey((key) => key + 1);
|
||||
setDirty(false);
|
||||
setStatus({
|
||||
success: true,
|
||||
msg: `Saved: ${res.singleRes.task_steps} steps per task, ${res.singleRes.reply_steps} per reply.`,
|
||||
});
|
||||
};
|
||||
|
||||
const discard = () => {
|
||||
setTaskSteps(String(overview?.task_steps ?? ""));
|
||||
setReplySteps(String(overview?.reply_steps ?? ""));
|
||||
setFormKey((key) => key + 1);
|
||||
setDirty(false);
|
||||
setStatus(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
save().catch(() => {});
|
||||
}}
|
||||
className="w-full gap-3! items-stretch"
|
||||
>
|
||||
<Stack className="w-full gap-3! items-stretch">
|
||||
<Input
|
||||
key={`task-${form_key}`}
|
||||
label="Tool steps per task"
|
||||
type="number"
|
||||
min={0}
|
||||
max={overview?.max}
|
||||
step={1}
|
||||
numberText
|
||||
rawNumber
|
||||
defaultValue={task_steps}
|
||||
onChange={(event) => {
|
||||
setTaskSteps(event.target.value);
|
||||
setDirty(true);
|
||||
setStatus(undefined);
|
||||
}}
|
||||
suffix={`steps`}
|
||||
info={`How many turns the model may take before it must stop and report, when it works on a task. 0 uses the default (${overview?.task_default ?? ""}).`}
|
||||
/>
|
||||
<Input
|
||||
key={`reply-${form_key}`}
|
||||
label="Tool steps per chat reply"
|
||||
type="number"
|
||||
min={0}
|
||||
max={overview?.max}
|
||||
step={1}
|
||||
numberText
|
||||
rawNumber
|
||||
defaultValue={reply_steps}
|
||||
onChange={(event) => {
|
||||
setReplySteps(event.target.value);
|
||||
setDirty(true);
|
||||
setStatus(undefined);
|
||||
}}
|
||||
suffix={`steps`}
|
||||
info={`How many turns a chat reply may take before it answers. Long work belongs in a task. 0 uses the default (${overview?.reply_default ?? ""}).`}
|
||||
/>
|
||||
</Stack>
|
||||
<SettingsSaveBar
|
||||
dirty={dirty}
|
||||
loading={loading}
|
||||
status={status}
|
||||
save_label="Save limits"
|
||||
onDiscard={discard}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import isPassThroughUser from "@/src/utils/rbac/is-pass-through-user";
|
||||
import SettingsGroup from "../../(partials)/settings-group";
|
||||
import RunLimitsForm from "../(partials)/run-limits-form";
|
||||
|
||||
/**
|
||||
* How long a bot may work on one task or reply before it reports. Only the
|
||||
* super admin sees it, since it changes every bot on the install.
|
||||
*/
|
||||
export default function RunLimitsSection() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
if (!isPassThroughUser({ user_types: pageProps?.user_types })) return null;
|
||||
|
||||
return (
|
||||
<SettingsGroup
|
||||
title="Run limits"
|
||||
description="How many turns a bot may take with its tools before it must stop and report. A task may take far more than a chat reply, since it does the long work."
|
||||
id="run-limits"
|
||||
>
|
||||
<RunLimitsForm limits={pageProps?.run_limits} />
|
||||
</SettingsGroup>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import AppearanceSection from "./(sections)/appearance-section";
|
||||
import NotificationsSection from "./(sections)/notifications-section";
|
||||
import PromptSection from "./(sections)/prompt-section";
|
||||
import RunLimitsSection from "./(sections)/run-limits-section";
|
||||
import StorageSection from "./(sections)/storage-section";
|
||||
import VersionSection from "./(sections)/version-section";
|
||||
import SettingsShell from "../(partials)/settings-shell";
|
||||
@@ -15,6 +16,7 @@ export default function AdminSettingsGeneralPage() {
|
||||
<AppearanceSection />
|
||||
<NotificationsSection />
|
||||
<PromptSection />
|
||||
<RunLimitsSection />
|
||||
<StorageSection />
|
||||
<VersionSection />
|
||||
</SettingsShell>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
|
||||
import grabRunLimitsOverview, {
|
||||
type RunLimitsOverview,
|
||||
} from "@/src/functions/backend/db/app-settings/grab-run-limits";
|
||||
import saveAppSetting from "@/src/functions/backend/db/app-settings/save-app-setting";
|
||||
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
|
||||
import { BotReplyMaxToolStepsSettingKey } from "@/src/dict/bot-dict";
|
||||
import { HarnessMaxToolStepsSettingKey } from "@/src/dict/harness-dict";
|
||||
import { SaveRunLimitsRequestSchema } from "@/src/schemas/harness-schema";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import type { APIResponseObject, BunextAPIRouteHandler } from "@moduletrace/bunext/types";
|
||||
|
||||
/**
|
||||
* Super admin only: how many model turns a task run and a chat reply may
|
||||
* take. Zero puts the code's default back for that one.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject<RunLimitsOverview>> = async ({
|
||||
req,
|
||||
body,
|
||||
}) => {
|
||||
try {
|
||||
if (req.method != "POST") throw new AppError(`Method not allowed`);
|
||||
|
||||
const user = await grabSuperAdminUser({ req });
|
||||
|
||||
if (!user?.id) throw new AppError(`Only a super admin can change the run limits.`);
|
||||
|
||||
const { task_steps, reply_steps } = parseWithSchema({
|
||||
schema: SaveRunLimitsRequestSchema,
|
||||
data: body,
|
||||
});
|
||||
|
||||
await saveAppSetting({
|
||||
key: HarnessMaxToolStepsSettingKey,
|
||||
value: task_steps > 0 ? String(task_steps) : null,
|
||||
});
|
||||
await saveAppSetting({
|
||||
key: BotReplyMaxToolStepsSettingKey,
|
||||
value: reply_steps > 0 ? String(reply_steps) : null,
|
||||
});
|
||||
|
||||
await logEvent({
|
||||
title: "Bot run limits changed",
|
||||
category: "system",
|
||||
user_id: user.id,
|
||||
plain_text: `User #${user.id} set the tool step limits to ${task_steps || "default"} per task and ${reply_steps || "default"} per chat reply.`,
|
||||
metadata: { task_steps, reply_steps },
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return { success: true, singleRes: await grabRunLimitsOverview() };
|
||||
} catch (error: unknown) {
|
||||
return { success: false, msg: safeErrorMessage({ error }) };
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import { Harnesses, type HarnessKind } from "@/src/dict/harness-dict";
|
||||
import {
|
||||
HarnessProviderKinds,
|
||||
@@ -96,6 +97,17 @@ export type SaveBotHarnessSettingsRequest = z.infer<
|
||||
typeof SaveBotHarnessSettingsRequestSchema
|
||||
>;
|
||||
|
||||
/**
|
||||
* The General settings' run limits: model turns a task run and a chat reply
|
||||
* may take. Zero puts the code's default back.
|
||||
*/
|
||||
export const SaveRunLimitsRequestSchema = z.object({
|
||||
task_steps: z.coerce.number().int().min(0).max(AppData.BotStepLimitMax),
|
||||
reply_steps: z.coerce.number().int().min(0).max(AppData.BotStepLimitMax),
|
||||
});
|
||||
|
||||
export type SaveRunLimitsRequest = z.infer<typeof SaveRunLimitsRequestSchema>;
|
||||
|
||||
export const PreviewVoiceRequestSchema = z.object({
|
||||
model_id: z.coerce.number().int().positive("Pick a speech model"),
|
||||
voice: z.string().trim().max(200).optional(),
|
||||
|
||||
@@ -1192,6 +1192,17 @@ export type PagePropsType = {
|
||||
setting_key: string;
|
||||
text: string;
|
||||
} | null;
|
||||
/**
|
||||
* Model turns a task run and a chat reply may take, with the code's
|
||||
* defaults (the General settings page).
|
||||
*/
|
||||
run_limits?: {
|
||||
task_steps: number;
|
||||
reply_steps: number;
|
||||
task_default: number;
|
||||
reply_default: number;
|
||||
max: number;
|
||||
} | null;
|
||||
/**
|
||||
* The release the server is running, for the General settings page.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import readStepLimit from "./read-step-limit";
|
||||
|
||||
const base = { fallback: 200, max: 1000 };
|
||||
|
||||
describe("readStepLimit", () => {
|
||||
test("uses a saved limit inside the allowed range", () => {
|
||||
expect(readStepLimit({ ...base, saved: "40" })).toBe(40);
|
||||
expect(readStepLimit({ ...base, saved: " 12 " })).toBe(12);
|
||||
expect(readStepLimit({ ...base, saved: "1" })).toBe(1);
|
||||
expect(readStepLimit({ ...base, saved: "1000" })).toBe(1000);
|
||||
});
|
||||
|
||||
test("falls back when nothing was saved", () => {
|
||||
expect(readStepLimit({ ...base, saved: undefined })).toBe(200);
|
||||
expect(readStepLimit({ ...base, saved: null })).toBe(200);
|
||||
expect(readStepLimit({ ...base, saved: "" })).toBe(200);
|
||||
expect(readStepLimit({ ...base, saved: " " })).toBe(200);
|
||||
});
|
||||
|
||||
test("falls back when the saved limit can't drive a run", () => {
|
||||
expect(readStepLimit({ ...base, saved: "0" })).toBe(200);
|
||||
expect(readStepLimit({ ...base, saved: "-3" })).toBe(200);
|
||||
expect(readStepLimit({ ...base, saved: "abc" })).toBe(200);
|
||||
expect(readStepLimit({ ...base, saved: "12.5" })).toBe(200);
|
||||
expect(readStepLimit({ ...base, saved: "NaN" })).toBe(200);
|
||||
expect(readStepLimit({ ...base, saved: "12steps" })).toBe(200);
|
||||
});
|
||||
|
||||
test("caps at the ceiling, never past it", () => {
|
||||
expect(readStepLimit({ ...base, saved: "1001" })).toBe(200);
|
||||
expect(readStepLimit({ ...base, saved: "999999" })).toBe(200);
|
||||
expect(readStepLimit({ fallback: 8, max: 50, saved: "51" })).toBe(8);
|
||||
expect(readStepLimit({ fallback: 8, max: 50, saved: "50" })).toBe(50);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
type Params = {
|
||||
/**
|
||||
* What the install saved, as it came out of `app_settings`: undefined or
|
||||
* empty when it never did.
|
||||
*/
|
||||
saved?: string | null;
|
||||
/**
|
||||
* The code's own limit, used whenever the saved one can't be trusted.
|
||||
*/
|
||||
fallback: number;
|
||||
/**
|
||||
* The most a saved limit may ask for.
|
||||
*/
|
||||
max: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads a tool-step limit saved in the settings, falling back to the code's
|
||||
* default when it is missing, not a whole number, below one, or above the
|
||||
* ceiling. The settings page and the saved row are both untrusted input:
|
||||
* without this a typo (or a zero) would stop every run before its first
|
||||
* turn, and a huge number would let one run alone run for hours.
|
||||
*/
|
||||
export default function readStepLimit({ saved, fallback, max }: Params) {
|
||||
const text = String(saved ?? "").trim();
|
||||
|
||||
if (!text) return fallback;
|
||||
|
||||
const value = Number(text);
|
||||
|
||||
if (!Number.isInteger(value) || value < 1 || value > max) return fallback;
|
||||
|
||||
return value;
|
||||
}
|
||||
Reference in New Issue
Block a user