mirror of
https://github.com/severian-dev/sucker.severian.dev.git
synced 2026-10-18 01:20:29 +00:00
Keep multi-turn conversations linked to their card
The first pass read the name from the last user turn and the token from the last assistant turn. Both are wrong once a conversation continues. Only sucker's *first* reply carries a conversation token — later ones say "Character updated" or "unchanged" with no token in them — so reading the last assistant turn lost the link three turns in and would have created a duplicate card on every message. tokenCandidates() now searches the whole conversation, still preferring whatever the user just typed, since pasting a token is deliberate. The name is the first user turn after the greeting, not the last one. That is what the old code read at messages[3], and unlike the last turn it stays put as messages are appended. Tests cover a three-turn conversation for both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NX2SsmUqQftWTZAY4BDuFa
This commit is contained in:
+14
-12
@@ -1,6 +1,10 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { selectMessageSlots, type Message } from "@/lib/messages";
|
||||
import {
|
||||
selectMessageSlots,
|
||||
tokenCandidates,
|
||||
type Message,
|
||||
} from "@/lib/messages";
|
||||
|
||||
interface CardVersion {
|
||||
version: number;
|
||||
@@ -115,7 +119,7 @@ function removePersonaTags(content: string): string {
|
||||
}
|
||||
|
||||
function extractCardData(messages: Message[]): ExtractedCard {
|
||||
const { system, greeting, lastUser } = selectMessageSlots(messages);
|
||||
const { system, greeting, nameSlot } = selectMessageSlots(messages);
|
||||
|
||||
if (!system?.content) {
|
||||
throw new Error("No system message: this request carries no character card.");
|
||||
@@ -126,7 +130,7 @@ function extractCardData(messages: Message[]): ExtractedCard {
|
||||
|
||||
const first_mes = greeting.content.replace(/{user}/g, "{{user}}");
|
||||
|
||||
const nameContent = lastUser?.content ?? "";
|
||||
const nameContent = nameSlot?.content ?? "";
|
||||
// If the name slot is actually a token, ignore it for naming purposes
|
||||
const tokenInNameSlot = parseConversationToken(nameContent);
|
||||
const lastColonIndex = nameContent.lastIndexOf(": ");
|
||||
@@ -346,16 +350,14 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// The token is either in what the user just typed, or in sucker's own
|
||||
// previous reply. Both are found by role, so a missing "." turn or a
|
||||
// longer conversation cannot shift them out from under us.
|
||||
const tokenSlots = selectMessageSlots(body.messages);
|
||||
const tokenCandidateUser: string | undefined = tokenSlots.lastUser?.content;
|
||||
const tokenCandidateAssistant: string | undefined =
|
||||
tokenSlots.lastAssistant?.content;
|
||||
// The token may be in what the user just typed, or in sucker's own first
|
||||
// reply from earlier in the conversation. Search every message rather than
|
||||
// a fixed pair of slots: only the first reply carries a token, so a long
|
||||
// chat would otherwise lose the link and duplicate the card.
|
||||
const providedConversationId =
|
||||
parseConversationToken(tokenCandidateUser || undefined) ||
|
||||
parseConversationToken(tokenCandidateAssistant || undefined);
|
||||
tokenCandidates(body.messages)
|
||||
.map((c) => parseConversationToken(c))
|
||||
.find((id): id is string => Boolean(id)) ?? null;
|
||||
|
||||
let existingCard: StoredCard | null = null;
|
||||
let linkedByToken = false;
|
||||
|
||||
Reference in New Issue
Block a user