/** * Telling people what actually went wrong. * * Every failure used to produce one message: "You dingus, read the directions * on sucker before trying again", with a note about context size. For a * truncated card that was fair. For anything else it blamed the user for * something they could not have done anything about. * * The split that matters is not explained-vs-unexplained, it is * **fixable-vs-not**: * * - A setup step or a truncated card is something a user can act on; * those messages are shown verbatim, and name the step. * - Everything else — a request with no card in it, no greeting, nothing * at all — cannot happen through Janitor, and if it does the user has no * way to cause or fix it. Telling them to "open the chat properly" would * be the old message wearing a new coat. Those say plainly that it is not * their fault and ask them to report it, carrying a short tag so the * report identifies which assumption broke. */ /** Where to send someone when sucker is at fault. */ export const CONTACT = "DM @lyseverian on Discord"; /** A failure the person who hit it can actually do something about. */ export class SuckerError extends Error { constructor(message: string) { super(message); this.name = "SuckerError"; } } /** * A request shaped in a way Janitor does not produce. Not the user's fault * and not theirs to fix; `detail` exists so their report says which * assumption broke, not so they can act on it. */ export class ImpossibleRequest extends Error { readonly detail: string; constructor(detail: string) { super(`impossible request: ${detail}`); this.name = "ImpossibleRequest"; this.detail = detail; } } /** * Both are checked by name as well as by identity: Next can bundle a module * into more than one chunk, and a mismatched class would quietly send every * classified error down the wrong path. */ const named = (error: unknown, name: string): boolean => typeof error === "object" && error !== null && (error as { name?: unknown }).name === name; export function isSuckerError(error: unknown): error is SuckerError { return error instanceof SuckerError || named(error, "SuckerError"); } export function isImpossibleRequest(error: unknown): error is ImpossibleRequest { return error instanceof ImpossibleRequest || named(error, "ImpossibleRequest"); } export function userFacingMessage(error: unknown): string { if (isSuckerError(error)) return error.message; const detail = isImpossibleRequest(error) ? ` (${error.detail})` : ""; return ( "Something went wrong that sucker has no explanation for. This one is " + "not your fault and there is nothing for you to fix — " + `please ${CONTACT} and say what you were pulling, because unreported ones ` + `do not get fixed.${detail}` ); } const PERSONA_OPEN = ""; const PERSONA_CLOSE = ""; /** What the persona marker says, or null if it is absent or cut off mid-tag. */ function personaDescription(content: string): string | null { const open = content.indexOf(PERSONA_OPEN); if (open === -1) return null; const close = content.indexOf(PERSONA_CLOSE, open + PERSONA_OPEN.length); if (close === -1) return null; return content.slice(open + PERSONA_OPEN.length, close); } /** A short, single-line quote of what the user actually set. */ function quoted(text: string): string { if (text.trim() === "") return "empty"; const flat = text.replace(/\s+/g, " ").trim(); return `"${flat.length > 40 ? flat.slice(0, 40) + "…" : flat}"`; } /** * Reads the two markers the two REQUIRED setup steps produce and says which * step is wrong — or, when a marker is simply gone, that the card was * probably cut short. Returns null when both are as they should be. * * `<.>` is the custom prompt and sits at the very start of the system * message, so if it is missing the prompt is not set: truncation cannot * remove the first three characters. `…` is the * persona description and sits after the card, so: * * - present with something other than "." inside → the description is * wrong. Certain, and the message quotes what it currently says. * - absent, or cut off mid-tag → either the persona is not set or Janitor * truncated the card because the bot is bigger than the context size. * Truncation is the common one, so it leads. */ export function diagnoseSetup(content: string): SuckerError | null { const hasPrompt = content.includes("<.>"); const persona = personaDescription(content); const personaOk = persona === "."; if (hasPrompt && personaOk) return null; const stillStuck = `Still stuck: ${CONTACT}.`; if (!hasPrompt) { let also = ""; if (persona === null) { also = " Your persona did not come through either: it needs the name " + "{{user}} and a description of just a dot."; } else if (!personaOk) { also = ` Your persona description is ${quoted(persona)} as well — it has to be just a dot.`; } return new SuckerError( "Sucker could not read that card: your custom prompt is not coming " + "through as <.> — set it in your API settings." + also + ` Save, then refresh Janitor, not sucker. ${stillStuck}` ); } if (persona !== null) { // Present but not "." — the one case that is certain. return new SuckerError( `Sucker could not read that card: your persona description is ${quoted(persona)} — ` + "it has to be just a dot, with the name {{user}}. Save, then refresh " + `Janitor, not sucker. ${stillStuck}` ); } // Prompt is fine and the persona marker is gone entirely: the end of the // card is missing, and the usual reason is that Janitor cut it short. return new SuckerError( "Sucker could not read that card: the end of it is missing. That usually " + "means the bot is bigger than your context size and Janitor cut it " + "short — raise Context Size in Generation Settings and try again. If it " + "is already generous, check your persona: name {{user}}, description " + `just a dot, then refresh Janitor. ${stillStuck}` ); } // 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 // which assumption broke. export const NO_CARD = new ImpossibleRequest("no system message in the request"); export const NO_GREETING = new ImpossibleRequest("no greeting in the request"); export const TOO_FEW_MESSAGES = new ImpossibleRequest("fewer than two messages in the request");