fix: repair orphaned user turns before embedded prompts
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user