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: 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: 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: 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: 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.
|
- 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.
|
- Discord: truncate skill command descriptions for slash command limits. (#1018) — thanks @evalexpr.
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ describe("runEmbeddedPiAgent", () => {
|
|||||||
expect(secondUserIndex).toBeGreaterThan(firstAssistantIndex);
|
expect(secondUserIndex).toBeGreaterThan(firstAssistantIndex);
|
||||||
expect(secondAssistantIndex).toBeGreaterThan(secondUserIndex);
|
expect(secondAssistantIndex).toBeGreaterThan(secondUserIndex);
|
||||||
}, 20_000);
|
}, 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 { SessionManager } = await import("@mariozechner/pi-coding-agent");
|
||||||
|
|
||||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
|
||||||
@@ -317,8 +317,40 @@ describe("runEmbeddedPiAgent", () => {
|
|||||||
agentDir,
|
agentDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.meta.error?.kind).toBe("role_ordering");
|
expect(result.meta.error).toBeUndefined();
|
||||||
expect(result.meta.error?.message).toMatch(/incorrect role information|roles must alternate/i);
|
expect(result.payloads?.length ?? 0).toBeGreaterThan(0);
|
||||||
expect(result.payloads?.[0]?.text).toContain("Message ordering conflict");
|
});
|
||||||
|
|
||||||
|
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,26 +441,22 @@ export async function runEmbeddedAttempt(
|
|||||||
const promptStartedAt = Date.now();
|
const promptStartedAt = Date.now();
|
||||||
log.debug(`embedded run prompt start: runId=${params.runId} sessionId=${params.sessionId}`);
|
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
|
// Repair orphaned trailing user messages so new prompts don't violate role ordering.
|
||||||
const lastMsg = activeSession.messages[activeSession.messages.length - 1];
|
const leafEntry = sessionManager.getLeafEntry();
|
||||||
const lastMsgRole =
|
if (leafEntry?.type === "message" && leafEntry.message.role === "user") {
|
||||||
lastMsg && typeof lastMsg === "object" ? (lastMsg as { role?: unknown }).role : undefined;
|
if (leafEntry.parentId) {
|
||||||
|
sessionManager.branch(leafEntry.parentId);
|
||||||
if (lastMsgRole === "user") {
|
} else {
|
||||||
// Last message was a user message. Adding another user message would create
|
sessionManager.resetLeaf();
|
||||||
// consecutive user turns, violating Anthropic's role ordering requirement.
|
}
|
||||||
// This can happen when:
|
const sessionContext = sessionManager.buildSessionContext();
|
||||||
// 1. A previous heartbeat didn't get a response
|
activeSession.agent.replaceMessages(sessionContext.messages);
|
||||||
// 2. A user message errored before getting an assistant response
|
|
||||||
// Skip this prompt to prevent "400 Incorrect role information" error.
|
|
||||||
log.warn(
|
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}`,
|
`runId=${params.runId} sessionId=${params.sessionId}`,
|
||||||
);
|
);
|
||||||
promptError = new Error(
|
}
|
||||||
"Incorrect role information: consecutive user messages would violate role ordering",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
await activeSession.prompt(params.prompt, { images: params.images });
|
await activeSession.prompt(params.prompt, { images: params.images });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -470,7 +466,6 @@ export async function runEmbeddedAttempt(
|
|||||||
`embedded run prompt end: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - promptStartedAt}`,
|
`embedded run prompt end: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - promptStartedAt}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await waitForCompactionRetry();
|
await waitForCompactionRetry();
|
||||||
|
|||||||
Reference in New Issue
Block a user