fix: sanitize user-facing errors and strip final tags

Co-authored-by: Drake Thomsen <drake.thomsen@example.com>
This commit is contained in:
Peter Steinberger
2026-01-16 03:00:40 +00:00
parent d9f9e93dee
commit 23e4ba845c
13 changed files with 239 additions and 31 deletions

View File

@@ -9,6 +9,7 @@ import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
import {
isCompactionFailureError,
isContextOverflowError,
sanitizeUserFacingText,
} from "../../agents/pi-embedded-helpers.js";
import {
resolveAgentIdFromSessionKey,
@@ -106,7 +107,10 @@ export async function runAgentTurnWithFallback(params: {
if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) {
return { skip: true };
}
return { text, skip: false };
if (!text) return { skip: true };
const sanitized = sanitizeUserFacingText(text);
if (!sanitized.trim()) return { skip: true };
return { text: sanitized, skip: false };
};
const handlePartialForTyping = async (payload: ReplyPayload): Promise<string | undefined> => {
const { text, skip } = normalizeStreamingText(payload);
@@ -366,6 +370,7 @@ export async function runAgentTurnWithFallback(params: {
/context.*overflow|too large|context window/i.test(message);
const isCompactionFailure = isCompactionFailureError(message);
const isSessionCorruption = /function call turn comes immediately after/i.test(message);
const isRoleOrderingError = /incorrect role information|roles must alternate/i.test(message);
if (
isCompactionFailure &&
@@ -427,7 +432,9 @@ export async function runAgentTurnWithFallback(params: {
payload: {
text: isContextOverflow
? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."
: `⚠️ Agent failed before reply: ${message}. Check gateway logs for details.`,
: isRoleOrderingError
? "⚠️ Message ordering conflict - please try again. If this persists, use /new to start a fresh session."
: `⚠️ Agent failed before reply: ${message}. Check gateway logs for details.`,
},
};
}

View File

@@ -213,4 +213,31 @@ describe("runReplyAgent typing (heartbeat)", () => {
}
}
});
it("returns friendly message for role ordering errors thrown as exceptions", async () => {
runEmbeddedPiAgentMock.mockImplementationOnce(async () => {
throw new Error("400 Incorrect role information");
});
const { run } = createMinimalRun({});
const res = await run();
expect(res).toMatchObject({
text: expect.stringContaining("Message ordering conflict"),
});
expect(res).toMatchObject({
text: expect.not.stringContaining("400"),
});
});
it("returns friendly message for 'roles must alternate' errors thrown as exceptions", async () => {
runEmbeddedPiAgentMock.mockImplementationOnce(async () => {
throw new Error('messages: roles must alternate between "user" and "assistant"');
});
const { run } = createMinimalRun({});
const res = await run();
expect(res).toMatchObject({
text: expect.stringContaining("Message ordering conflict"),
});
});
});

View File

@@ -1,6 +1,7 @@
import { stripHeartbeatToken } from "../heartbeat.js";
import { HEARTBEAT_TOKEN, isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
import type { ReplyPayload } from "../types.js";
import { sanitizeUserFacingText } from "../../agents/pi-embedded-helpers.js";
import {
resolveResponsePrefixTemplate,
type ResponsePrefixContext,
@@ -42,6 +43,11 @@ export function normalizeReplyPayload(
text = stripped.text;
}
if (text) {
text = sanitizeUserFacingText(text);
}
if (!text?.trim() && !hasMedia) return null;
// Resolve template variables in responsePrefix if context is provided
const effectivePrefix = opts.responsePrefixContext
? resolveResponsePrefixTemplate(opts.responsePrefix, opts.responsePrefixContext)