Files
stack/packages/cli/src/limits.mjs
T
jason.woltje 98d29769e2 bus, cli: tester reads and the W1-W7 tester writes, recorded as tester; scripts/mosaic sends plugins, name and names to the CLI (row 175, #1673)
TESTER_VERBS gains usage, names, limits, plugins, instances, project.list,
thread.list, name.set and project.create. A tester reads limits, plugins
and instances and sends plugins.set, instances.set and routes.set, each
logged with actor tester. The project file verbs refuse a tester
tester-refused. The broker and CLI drop the [mos test]
prefix rule (mos-0872 Q1). The CLI takes --tester on the seven reads and
the W1-W7 write forms and refuses it (exit 4) elsewhere, bus start and
stop included.

project init --tester checks the project area, refuses the area root
itself, binds the tester with one agents read, then commits the layout as
tester (mos-0872 Q2). scripts/mosaic sends plugins, name, names, follow,
threads and thread to packages/cli (sage-1081). The row 164 pins are
untouched.

Built by Dewey on 9e74925c2 (three commits to 989699f7c, manifest
2e30f2a1...29d5f), approved by Filbert in round 3 (#1673 comment 29279),
Sage's gate green on 716ddcf1e plus the patch.
2026-10-19 19:04:50 -05:00

95 lines
5.1 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.
export const LIMITS_USAGE = "mosaic limits [--business ID] [--json] [--tester] | set --file PATCH [--business ID] | " +
"reset PATH... [--business ID] | migrate --file ORGANIZATION --models MAPPING [--roles-dir DIR]; " +
"PATCH keys: revision, limits, models, instances, reset, assistants";
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;
}
if (!["view", "set", "reset"].includes(verb) || values["--models"] || values["--roles-dir"] ||
(verb !== "set" && values["--file"]) || (verb === "set" && !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 === "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);
}
}