mirror of
https://github.com/severian-dev/sucker.severian.dev.git
synced 2026-10-18 01:20:29 +00:00
Diagnose setup by what is certain, and deliver errors the way the client asked
Review of the branch found the persona diagnosis was mostly truncation wearing a name tag. The <.> marker sits at the start of the system message and the persona marker after the card, so a card Janitor cut short arrives with the prompt present and the persona gone — and was told its persona was wrong, with truncation as an afterthought. The original message was only ever about context size, which is the best evidence there is about what actually happens. diagnoseSetup() now says only what the markers make certain. A missing <.> is a missing prompt, since truncation cannot remove the first three characters. A persona marker that is present but not "." is a wrong description, quoted back so there is nothing to guess. A persona marker that is gone, or cut off mid-tag, leads with truncation and mentions the persona second. The error path decided "streaming?" from the Accept header while the success path used body.stream, so a client that sent stream:true without that header got JSON back for every error and never displayed it. The flag is hoisted above the try; the header is only a fallback for when the body could not be read. Token search is restricted to after the greeting, in order. A card built from a sucker-proxied chat could carry an expired token in its text, and a lookup on it would fail forever. Conversation order also means the token a chat was started with always wins over one pasted later. The system fallback now takes a role-less first message only; a request with roles but no system turn is reported as such rather than misdiagnosed as a missing prompt. The echo feature reads the parser's lastUser instead of its own case-sensitive lookup. "Nothing was saved" is dropped from the unknown-error message, since it is not guaranteed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NX2SsmUqQftWTZAY4BDuFa
This commit is contained in:
+77
-29
@@ -9,8 +9,8 @@
|
||||
* The split that matters is not explained-vs-unexplained, it is
|
||||
* **fixable-vs-not**:
|
||||
*
|
||||
* - A truncated card is the one failure a user can act on: raise the
|
||||
* context size. That message is shown verbatim.
|
||||
* - 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
|
||||
@@ -66,42 +66,90 @@ 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, and nothing was " +
|
||||
`saved. This one is not your fault and there is nothing for you to fix — ` +
|
||||
"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}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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";
|
||||
const PERSONA_OPEN = "<UserPersona>";
|
||||
const PERSONA_CLOSE = "</UserPersona>";
|
||||
|
||||
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;
|
||||
/** 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. `<UserPersona>…</UserPersona>` 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: ${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}.`
|
||||
"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}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user