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)));
}
});