From 533f2d80939c1bb28449f1379047ee846c81349c Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Wed, 23 Sep 2026 00:35:21 +0100 Subject: [PATCH] mj-queue: cap the --no list, and refuse to build past it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gryphon fix added six armour negatives and three weapon ones, which took the prompt to eight --no terms. This project has a recorded case where ten or more never returned a grid at all while the same prompt with four came back in two minutes — and a job that never arrives is indistinguishable from a moderation refusal, because those are ephemeral and never appear in the channel either. It cost twenty minutes of wondering whether the account was out of fast hours, and the note about it was written down precisely so it would not be diagnosed from scratch a second time. I walked into it anyway. Trimmed to three armour terms and one weapon term, which puts every prompt at five. promptFor now throws above six and names the offending terms, so the next person to reach for a long negative list is stopped at the tool rather than by a half-hour stall. All 204 specs build under the cap. Co-Authored-By: Claude Opus 5 --- tools/mj-queue.mjs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tools/mj-queue.mjs b/tools/mj-queue.mjs index 0bcf735..a7d34c4 100644 --- a/tools/mj-queue.mjs +++ b/tools/mj-queue.mjs @@ -134,12 +134,17 @@ const armourWords = (n, era = "antique") => bandOf(Number(n) || 0, [ function negativesFor(spec, era, isPerson) { if (isPerson) return []; const out = []; - if (era !== "future" && era !== "anomalous") { - out.push("worn armour", "plate armour", "armour plating", "machinery", "robot", "mecha"); - } + /* SHORT, deliberately. The base style already carries two negatives, and this project + has a recorded failure where prompts with ten or more --no terms never returned a + grid at all while the same prompt with four came back in two minutes — a stall that + is indistinguishable from a moderation refusal and so gets misdiagnosed every time. + The first version of this function pushed six armour terms and three weapon terms, + which took the gryphon to eight and into that band. Three terms and one; steer with + the prompt, which is what the armourWords rewrite above is for. */ + if (era !== "future" && era !== "anomalous") out.push("plate armour", "machinery", "robot"); // "no weapon — it uses its own limbs" is already in the prompt, positively. An animal // is still handed a spear often enough to be worth stating twice. - if (!(spec.weapons ?? []).length) out.push("weapons", "swords", "armour straps"); + if (!(spec.weapons ?? []).length) out.push("weapons"); return out; } @@ -311,6 +316,21 @@ export function promptFor(spec, kind = "npc") { const base = expand(clauses.join(", ").replace(/\s+/g, " ").trim()); const extra = negativesFor(spec, era, isPerson); const prompt = extra.length ? `${base}, ${extra.join(", ")}` : base; + + /* A prompt with too many --no terms does not fail, which is the problem: it never + returns a grid at all, and a job that never arrives looks exactly like a moderation + refusal — those are ephemeral and never appear in the channel either. The recorded + case cost twenty minutes of wondering whether the account was out of fast hours. + Refuse to build one, and say which terms, rather than letting it be diagnosed again + from scratch. Six is under the observed cliff with room to spare. */ + const NEGATIVE_CAP = 6; + const negatives = (prompt.split("--no ")[1] ?? "").split(",").map(x => x.trim()).filter(Boolean); + if (negatives.length > NEGATIVE_CAP) { + throw new Error(`${spec.key}: ${negatives.length} --no terms (cap ${NEGATIVE_CAP}) — ` + + `a long negative list stalls the job silently rather than failing it. ` + + `Steer in the prompt instead. Terms: ${negatives.join(", ")}`); + } + const warnings = BANNED.filter(b => prompt.toLowerCase().includes(b)); return { prompt, token, warnings }; }