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:
2026-10-23 16:20:38 +01:00
parent 06586a6dc8
commit 024d847b9a
16 changed files with 420 additions and 5 deletions
+36
View File
@@ -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);
});
});
+34
View File
@@ -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;
}