diff --git a/codec_chat.html b/codec_chat.html
index 746ff6c..cc53e37 100644
--- a/codec_chat.html
+++ b/codec_chat.html
@@ -888,7 +888,7 @@
// (also stripping an UNCLOSED tail — a cut/degenerate stream
// must never dump raw reasoning into the bubble) > honest glitch notice
// with the reasoning kept in the reveal panel.
- var answer=(pf.answer&&pf.answer.trim())||raw.replace(/[\s\S]*?<\/thinking>/i,'').replace(/[\s\S]*$/i,'').replace(/###\s*FINAL ANSWER:?/i,'').trim();
+ var answer=(pf.answer&&pf.answer.trim())||raw.replace(/[\s\S]*?<\/thinking>/i,'').replace(/[\s\S]*$/i,'').replace(/###\s*FINAL ANSWER:?/i,'').replace(/<\/?final\s*answer>/gi,'').trim();
if(!answer&&pf.think&&pf.think.trim()){answer='*My reasoning ran on without reaching a final answer — a local-model glitch, not a real reply. The train of thought is below; please ask again.*'}
if(answer){
div.remove();var md=addMessage('assistant',answer);
@@ -1703,7 +1703,7 @@
var pf2=parseScaffold(raw);
// Same fallback order as the primary handler: never dump an unclosed
// tail into the bubble; keep reasoning in the reveal panel.
- var answer2=(pf2.answer&&pf2.answer.trim())||raw.replace(/[\s\S]*?<\/thinking>/i,'').replace(/[\s\S]*$/i,'').replace(/###\s*FINAL ANSWER:?/i,'').trim();
+ var answer2=(pf2.answer&&pf2.answer.trim())||raw.replace(/[\s\S]*?<\/thinking>/i,'').replace(/[\s\S]*$/i,'').replace(/###\s*FINAL ANSWER:?/i,'').replace(/<\/?final\s*answer>/gi,'').trim();
if(!answer2&&pf2.think&&pf2.think.trim()){answer2='*My reasoning ran on without reaching a final answer — a local-model glitch, not a real reply. The train of thought is below; please ask again.*'}
if(answer2){div.remove();var md2=addMessage('assistant',answer2);if(thinkingEnabled&&pf2.think&&pf2.think.trim()&&md2){var fp2=makeToT(md2,'Train of thought',false);totSet(fp2,pf2.think.trim())}chatHist.push({role:'assistant',content:answer2});saveMessages([{role:'assistant',content:answer2}])}
else{bubble.innerHTML='No response'}
diff --git a/routes/chat.py b/routes/chat.py
index 9544429..91db249 100644
--- a/routes/chat.py
+++ b/routes/chat.py
@@ -1110,16 +1110,37 @@ async def _skill_stream():
f"Treat it as the current working directory for this conversation."
)
+ # A persona model (Fréd, M Corpus, Mwen — config extra_models[].system_prompt*)
+ # OWNS the turn: it must speak only as itself. The frontend still ships its
+ # generic "You are CODEC Deep Chat — a J.A.R.V.I.S.-class AI…" system
+ # message; gluing that onto the persona gave the 4B two conflicting
+ # identities (it answered in the generic voice and confabulated an
+ # attachment it never read). So for a persona, REPLACE the client system
+ # message with the persona prompt instead of concatenating.
+ _persona_active = False
+ try:
+ import codec_models as _cmp
+ _persona_active = bool(_cmp.model_extras().get("system_prompt"))
+ except Exception as _e:
+ log.debug("persona check failed: %s", _e)
+
# Prepend system message (or replace existing one)
if messages and messages[0].get("role") == "system":
- messages[0]["content"] = sys_prompt + "\n\n" + messages[0]["content"]
+ messages[0]["content"] = (
+ sys_prompt if _persona_active
+ else sys_prompt + "\n\n" + messages[0]["content"]
+ )
else:
messages.insert(0, {"role": "system", "content": sys_prompt})
# Think mode: append the reasoning scaffold LAST so it wins over the base
# prompt's "answer directly"/emoji rules. Frontend sends reason_scaffold
# = Think-toggle state; it parses the resulting /### FINAL ANSWER.
- if body.get("reason_scaffold") and messages and messages[0].get("role") == "system":
+ # Skipped for persona models: a small fine-tune renders the format as its
+ # own variant the UI stripper misses, and it fights the
+ # persona's voice.
+ if (body.get("reason_scaffold") and not _persona_active
+ and messages and messages[0].get("role") == "system"):
messages[0]["content"] += _REASON_SCAFFOLD
stream_mode = body.get("stream", False)