From a8b3669c17a2d0c0339da62ebec4c0dd680aa5a5 Mon Sep 17 00:00:00 2001 From: Severian Date: Fri, 11 Sep 2026 06:06:36 +0800 Subject: [PATCH] Name the setting that did not come through MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The instructions mark exactly two steps REQUIRED, and the two markers sucker looks for are those two steps. So a failure can say which one is missing — the custom prompt, the persona, or both — instead of sending someone back to re-read a page they have already followed. Truncation causes the same symptom, so it is still named, but second: it only applies once the settings really are set. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NX2SsmUqQftWTZAY4BDuFa --- src/app/api/proxy/route.ts | 13 +++++++------ src/lib/errors.ts | 37 +++++++++++++++++++++++++++++-------- tests/errors.test.ts | 28 ++++++++++++++++++++++------ 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/app/api/proxy/route.ts b/src/app/api/proxy/route.ts index 6aacc35..45a60af 100644 --- a/src/app/api/proxy/route.ts +++ b/src/app/api/proxy/route.ts @@ -9,7 +9,7 @@ import { NO_CARD, NO_GREETING, TOO_FEW_MESSAGES, - TRUNCATED_CARD, + missingSetup, userFacingMessage, } from "@/lib/errors"; @@ -165,11 +165,12 @@ function extractCardData(messages: Message[]): ExtractedCard { `Name extraction: user="${nameFromUser}", inferred="${inferredName}", tracking="${cleanTrackingName}", display="${displayName}"` ); - if ( - !content.includes("<.>") || - !content.includes(".") - ) { - throw TRUNCATED_CARD; + // Each marker is one of the two REQUIRED steps in the instructions, so the + // error can name the one that did not arrive. + const hasPrompt = content.includes("<.>"); + const hasPersona = content.includes("."); + if (!hasPrompt || !hasPersona) { + throw missingSetup(hasPrompt, hasPersona); } content = content.replace("<.>", ""); diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 25f2fe1..7de3273 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -74,15 +74,36 @@ export function userFacingMessage(error: unknown): string { } /** - * The one failure a user can act on: Janitor cut the card short because the - * bot is bigger than their context size, so the markers sucker reads are gone. + * The two markers sucker reads map one-to-one onto the two steps the + * instructions mark REQUIRED, so a missing one names the setting that is not + * coming through rather than sending people back to re-read everything. + * + * Truncation produces the same symptom — Janitor cuts a card bigger than the + * context size and takes the markers with it — so it is named too, second, + * because it only applies once the settings really are right. */ -export const TRUNCATED_CARD = new SuckerError( - "The card came through incomplete — the markers sucker looks for are not in " + - "it. That usually means the bot is larger than your context size and " + - "Janitor cut it short, so raise Context Size in Generation Settings and " + - `try again. If it is already generous, re-read the directions on sucker, and ${CONTACT} if it still will not go.` -); +export function missingSetup(hasPrompt: boolean, hasPersona: boolean): SuckerError { + const PROMPT = "your custom prompt is not coming through as <.> — set it in your API settings"; + const PERSONA = + "your persona is not coming through — it needs the name {{user}} and a description of just a dot"; + + let what: string; + if (!hasPrompt && !hasPersona) { + what = + "neither of the two required settings came through: the custom prompt " + + "has to be <.>, and your persona needs the name {{user}} with a " + + "description of just a dot"; + } else { + what = hasPrompt ? PERSONA : PROMPT; + } + + return new SuckerError( + `Sucker could not read that card: ${what}. Save it, then refresh Janitor — ` + + "the settings page, not sucker. If it is already set that way, the bot is " + + "probably bigger than your context size and Janitor cut the card short, " + + `so raise Context Size in Generation Settings. Still stuck: ${CONTACT}.` + ); +} // None of these can happen through Janitor. They exist so that if one ever // does, the reply is honest about whose problem it is and the report says diff --git a/tests/errors.test.ts b/tests/errors.test.ts index 2b4983b..c5e0c8a 100644 --- a/tests/errors.test.ts +++ b/tests/errors.test.ts @@ -10,16 +10,32 @@ import { NO_GREETING, SuckerError, TOO_FEW_MESSAGES, - TRUNCATED_CARD, + missingSetup, isImpossibleRequest, isSuckerError, userFacingMessage, } from "../src/lib/errors.ts"; -test("the one fixable failure is shown as written, with the advice that fixes it", () => { - assert.equal(userFacingMessage(TRUNCATED_CARD), TRUNCATED_CARD.message); - assert.match(TRUNCATED_CARD.message, /Context Size/); - assert.ok(TRUNCATED_CARD.message.includes(CONTACT)); +test("a missing setting is named, not left to the reader to find", () => { + const noPrompt = missingSetup(false, true).message; + assert.match(noPrompt, /custom prompt is not coming through as <\.>/); + assert.ok(!/persona needs the name/.test(noPrompt), "must not blame the setting that is fine"); + + const noPersona = missingSetup(true, false).message; + assert.match(noPersona, /persona is not coming through/); + assert.ok(!/custom prompt is not coming through/.test(noPersona)); + + const neither = missingSetup(false, false).message; + assert.match(neither, /neither of the two required settings/); +}); + +test("every setup message still offers truncation as the second cause", () => { + for (const m of [missingSetup(false, true), missingSetup(true, false), missingSetup(false, false)]) { + assert.match(m.message, /Context Size/); + assert.ok(m.message.includes(CONTACT)); + // Shown verbatim: this is the one class of failure a user can act on. + assert.equal(userFacingMessage(m), m.message); + } }); test("shapes Janitor cannot produce never tell the user to fix them", () => { @@ -47,7 +63,7 @@ test("a genuine bug reports itself without leaking internals", () => { }); test("nobody is called a dingus", () => { - for (const e of [TRUNCATED_CARD, NO_CARD, NO_GREETING, TOO_FEW_MESSAGES, new Error("x")]) { + for (const e of [missingSetup(false, false), NO_CARD, NO_GREETING, TOO_FEW_MESSAGES, new Error("x")]) { assert.ok(!/dingus/i.test(userFacingMessage(e))); } });