fix: repair orphaned user turns before embedded prompts

This commit is contained in:
Peter Steinberger
2026-01-16 20:52:12 +00:00
parent 56efbce31e
commit 0dcffcd5b0
3 changed files with 57 additions and 29 deletions

View File

@@ -78,6 +78,7 @@
- Fix: normalize pairing CLI aliases, allow extension channels, and harden Zalo webhook payload parsing. (#991) — thanks @longmaba.
- Fix: allow local Tailscale Serve hostnames without treating tailnet clients as direct. (#885) — thanks @oswalpalash.
- Fix: reset sessions after role-ordering conflicts to recover from consecutive user turns. (#998)
- Fix: repair orphaned user turns before embedded prompts to avoid role-ordering lockouts. (#1026) — thanks @odrobnik.
- Fix: keep background exec aborts from killing backgrounded sessions while honoring timeouts.
- Fix: use local auth for gateway security probe unless remote mode has a URL. (#1011) — thanks @ivanrvpereira.
- Discord: truncate skill command descriptions for slash command limits. (#1018) — thanks @evalexpr.

View File

@@ -261,7 +261,7 @@ describe("runEmbeddedPiAgent", () => {
expect(secondUserIndex).toBeGreaterThan(firstAssistantIndex);
expect(secondAssistantIndex).toBeGreaterThan(secondUserIndex);
}, 20_000);
it("returns role ordering error when session ends with a user turn", async () => {
it("repairs orphaned user messages and continues", async () => {
const { SessionManager } = await import("@mariozechner/pi-coding-agent");
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
@@ -317,8 +317,40 @@ describe("runEmbeddedPiAgent", () => {
agentDir,
});
expect(result.meta.error?.kind).toBe("role_ordering");
expect(result.meta.error?.message).toMatch(/incorrect role information|roles must alternate/i);
expect(result.payloads?.[0]?.text).toContain("Message ordering conflict");
expect(result.meta.error).toBeUndefined();
expect(result.payloads?.length ?? 0).toBeGreaterThan(0);
});
it("repairs orphaned single-user sessions and continues", async () => {
const { SessionManager } = await import("@mariozechner/pi-coding-agent");
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-"));
const sessionFile = path.join(workspaceDir, "session.jsonl");
const sessionManager = SessionManager.open(sessionFile);
sessionManager.appendMessage({
role: "user",
content: [{ type: "text", text: "seed user only" }],
});
const cfg = makeOpenAiConfig(["mock-1"]);
await ensureModels(cfg, agentDir);
const result = await runEmbeddedPiAgent({
sessionId: "session:test",
sessionKey: "agent:main:main",
sessionFile,
workspaceDir,
config: cfg,
prompt: "hello",
provider: "openai",
model: "mock-1",
timeoutMs: 5_000,
agentDir,
});
expect(result.meta.error).toBeUndefined();
expect(result.payloads?.length ?? 0).toBeGreaterThan(0);
});
});

View File

@@ -441,35 +441,30 @@ export async function runEmbeddedAttempt(
const promptStartedAt = Date.now();
log.debug(`embedded run prompt start: runId=${params.runId} sessionId=${params.sessionId}`);
// Check if last message is a user message to prevent consecutive user turns
const lastMsg = activeSession.messages[activeSession.messages.length - 1];
const lastMsgRole =
lastMsg && typeof lastMsg === "object" ? (lastMsg as { role?: unknown }).role : undefined;
if (lastMsgRole === "user") {
// Last message was a user message. Adding another user message would create
// consecutive user turns, violating Anthropic's role ordering requirement.
// This can happen when:
// 1. A previous heartbeat didn't get a response
// 2. A user message errored before getting an assistant response
// Skip this prompt to prevent "400 Incorrect role information" error.
// Repair orphaned trailing user messages so new prompts don't violate role ordering.
const leafEntry = sessionManager.getLeafEntry();
if (leafEntry?.type === "message" && leafEntry.message.role === "user") {
if (leafEntry.parentId) {
sessionManager.branch(leafEntry.parentId);
} else {
sessionManager.resetLeaf();
}
const sessionContext = sessionManager.buildSessionContext();
activeSession.agent.replaceMessages(sessionContext.messages);
log.warn(
`Skipping prompt because last message is a user message (would create consecutive user turns). ` +
`Removed orphaned user message to prevent consecutive user turns. ` +
`runId=${params.runId} sessionId=${params.sessionId}`,
);
promptError = new Error(
"Incorrect role information: consecutive user messages would violate role ordering",
}
try {
await activeSession.prompt(params.prompt, { images: params.images });
} catch (err) {
promptError = err;
} finally {
log.debug(
`embedded run prompt end: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - promptStartedAt}`,
);
} else {
try {
await activeSession.prompt(params.prompt, { images: params.images });
} catch (err) {
promptError = err;
} finally {
log.debug(
`embedded run prompt end: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - promptStartedAt}`,
);
}
}
try {