Files
stack/packages/cli/src/limits.mjs
T
jason.woltje a1b72432cd launchpad: row 217 C2-06 work in progress, UNTESTED, handed to Beakley (mos-1500)
Guard, limits, M11 fence, broker guard/page store/check event, BusError
reason, runtime wiring and the CLI replies verb. No runner work, no tests,
nothing run. BUILD.md lists the rulings, the row 225 contract and what is left.
2026-10-24 05:39:16 -05:00

102 lines
5.6 KiB
JavaScript

// REQ-LIMIT-1: human settings; decision 145 migration is a separate print-only read.
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { loadSystem } from "./config.mjs";
import { systemVars } from "../../business/src/index.mjs";
import { CliError } from "./errors.mjs";
import { migrateLaunch } from "../../business/src/migrate-launch.mjs";
// Row 155 (#1649, Q169): a PATCH may carry `assistants`, the assistant's vars, tracker and definition.
// C2-06 M11: `replies --file LIMITS` replaces replyLimits with the file's object, at the current revision.
export const LIMITS_USAGE = "mosaic limits [--business ID] [--json] [--tester] | set --file PATCH [--business ID] | " +
"reset PATH... [--business ID] | replies --file LIMITS [--business ID] | migrate --file ORGANIZATION --models MAPPING [--roles-dir DIR]; " +
"PATCH keys: revision, limits, models, instances, reset, assistants; LIMITS keys: requireCitation, citationsPerReply";
export const DAILY_NOTE = "Daily quotas count only this stack's own use of the account, not its use anywhere else.";
// Row 125 (#1619, REQ-LIMIT-3, REQ-LIMIT-4): one line per token limit, its path in the words
// `mosaic limits reset` takes. A limit no switched-on check counts reads "not enforced".
export function formatTokens(tokens) {
if (!Array.isArray(tokens) || tokens.length === 0) return "token limits: none\n";
const lines = tokens.map(t => {
const head = `${t.path.join(" ")}: limit ${t.limit} since ${t.since}`;
if (t.status === "counted") {
const kinds = Object.entries(t.kinds).map(([k, n]) => `${k} ${n}`).join(", ");
return `${head}, count ${t.count} (${kinds}), unknown turns ${t.unknownTurns}`;
}
if (t.status === "not-enforced") return `${head}, not enforced${t.reason ? `: ${t.reason}` : ""}`;
return `${head}, check failed: ${t.code}${t.check ? ` (${t.check})` : ""}`;
});
// Row 126 (#1620): a daily quota counts only the records this stack wrote. Said once, after the list.
const daily = tokens.some(t => t.path?.[1] === "daily") ? `${DAILY_NOTE}\n` : "";
return `token limits:\n${lines.map(l => ` ${l}`).join("\n")}\n${daily}`;
}
export function parseLimits(args) {
const out = { positional: [], flags: new Set(), values: {} };
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--json" || arg === "--tester") out.flags.add(arg);
else if (["--file", "--models", "--roles-dir", "--business"].includes(arg)) {
if (!args[i + 1] || Object.hasOwn(out.values, arg)) throw new CliError(LIMITS_USAGE, 4);
out.values[arg] = args[++i];
} else if (arg.startsWith("--")) throw new CliError(LIMITS_USAGE, 4);
else out.positional.push(arg);
}
return out;
}
export async function limitsCommand(args, io, callFor) {
const parsed = parseLimits(args);
const verb = parsed.positional[0] ?? "view";
const values = parsed.values;
if (parsed.positional.length > 1 && verb !== "reset") throw new CliError(LIMITS_USAGE, 4);
// Row 175 (#1673, Q173): the tester reads limits; set, reset and migrate stay the human's.
if (parsed.flags.has("--tester") && verb !== "view") throw new CliError(`--tester takes only the read\n${LIMITS_USAGE}`, 4);
try {
if (verb === "migrate") {
if (!values["--file"] || !values["--models"] || values["--business"]) {
throw new CliError(LIMITS_USAGE, 4);
}
const file = values["--file"];
const raw = JSON.parse(readFileSync(file, "utf8"));
const mapping = JSON.parse(readFileSync(values["--models"], "utf8"));
const rolesDir = values["--roles-dir"] ?? fileURLToPath(new URL("../../../roles", import.meta.url));
const inherits = raw.launch?.max && Object.values(raw.roles ?? {}).some(role => role.vars?.model === undefined);
const system = inherits ? systemVars(loadSystem({ env: io.env })) : null;
const converted = migrateLaunch(raw, mapping, file, { rolesDir, system });
io.stdout.write(JSON.stringify(converted, null, 2) + "\n");
return;
}
const fileVerb = verb === "set" || verb === "replies";
if (!["view", "set", "reset", "replies"].includes(verb) || values["--models"] || values["--roles-dir"] ||
(!fileVerb && values["--file"]) || (fileVerb && !values["--file"]) ||
(verb === "reset" && parsed.positional.length < 2)) {
throw new CliError(LIMITS_USAGE, 4);
}
const call = callFor(parsed);
// Row 125 (#1619, REQ-LIMIT-3): `reset tokens providers anthropic` restarts that limit's count
// from now. The path is one word per level, so a model name may hold dots or slashes. The
// result's `tokens` lists each token limit: counted, not-enforced or check-failed.
let result;
if (verb === "set") result = await call("limits.set", JSON.parse(readFileSync(values["--file"], "utf8")));
else if (verb === "replies") {
const replyLimits = JSON.parse(readFileSync(values["--file"], "utf8"));
const { revision } = await call("limits", {});
result = await call("library.limits.set", { revision, replyLimits });
}
else if (verb === "reset") {
const { revision } = await call("limits", {});
result = await call("limits.set", { revision, reset: [parsed.positional.slice(1)] });
} else result = await call("limits", {});
if (parsed.flags.has("--json")) io.stdout.write(JSON.stringify(result, null, 2) + "\n");
else {
const { tokens, ...rest } = result ?? {};
io.stdout.write(JSON.stringify(rest, null, 2) + "\n" + formatTokens(tokens));
}
} catch (error) {
if (error instanceof CliError) throw error;
throw new CliError(error.message, 2);
}
}