mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 10:12:44 +00:00
Keep a Claude rewind anchor off subagent messages (#5289)
Task subagent frames stream on the same query as the conversation, and rememberTranscriptProgress recorded any assistant frame as the current turn's rewind anchor. When a turn's last assistant frame came from a subagent, the anchor held a uuid that lives on the subagent's sidechain rather than on the session's own chain, so forkSession rejected it with "Message <uuid> not found in session" and every later rewind in that conversation failed. Skip frames that carry a parent_tool_use_id when recording anchors. The persisted history path already skips sidechain entries; the live stream now applies the same rule. Closes #5284 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import type { AgentSession } from "../../agent-sdk-types.js";
|
||||
const SESSION_ID = "session-1";
|
||||
|
||||
/** Claude Code replies to a turn either with an assistant message or with nothing at all. */
|
||||
type TurnOutcome = { assistantMessageId: string } | "no-response";
|
||||
type TurnOutcome = { assistantMessageId: string; subagentMessageId?: string } | "no-response";
|
||||
|
||||
function initMessage(): Record<string, unknown> {
|
||||
return {
|
||||
@@ -40,6 +40,21 @@ function assistantReply(uuid: string): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A message from a Task subagent. The CLI streams these on the same query as the main
|
||||
* conversation, and their uuids live on the subagent's sidechain, not on the session's
|
||||
* own message chain, so `forkSession` cannot resolve one.
|
||||
*/
|
||||
function subagentReply(uuid: string): Record<string, unknown> {
|
||||
return {
|
||||
type: "assistant",
|
||||
uuid,
|
||||
session_id: SESSION_ID,
|
||||
parent_tool_use_id: "toolu_subagent_1",
|
||||
message: { id: uuid, role: "assistant", content: [{ type: "text", text: "subagent work" }] },
|
||||
};
|
||||
}
|
||||
|
||||
function successResult(): Record<string, unknown> {
|
||||
return {
|
||||
type: "result",
|
||||
@@ -110,6 +125,9 @@ function createConversation(outcomes: TurnOutcome[]): Conversation {
|
||||
continue;
|
||||
}
|
||||
enqueue(assistantReply(outcome.assistantMessageId));
|
||||
if (outcome.subagentMessageId) {
|
||||
enqueue(subagentReply(outcome.subagentMessageId));
|
||||
}
|
||||
enqueue(successResult());
|
||||
}
|
||||
closedRef.value = true;
|
||||
@@ -229,3 +247,23 @@ describe("Claude rewind across a turn that produced no response", () => {
|
||||
expect(rewindSdk.recordedForks).toEqual([{ upToMessageId: "assistant-1" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Claude rewind after a turn whose last assistant message came from a subagent", () => {
|
||||
test("forks at the turn's own reply, not at the subagent message", async () => {
|
||||
const conversation = createConversation([
|
||||
{ assistantMessageId: "assistant-1", subagentMessageId: "subagent-1" },
|
||||
{ assistantMessageId: "assistant-2" },
|
||||
]);
|
||||
const rewindSdk = new FakeClaudeSdk();
|
||||
const session = await createSession(conversation, rewindSdk);
|
||||
|
||||
try {
|
||||
await runTurns(session, 2);
|
||||
await session.revertConversation?.({ messageId: conversation.userMessageIds[1] });
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
|
||||
expect(rewindSdk.recordedForks).toEqual([{ upToMessageId: "assistant-1" }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3054,6 +3054,13 @@ class ClaudeAgentSession implements AgentSession {
|
||||
if (!messageId) {
|
||||
return;
|
||||
}
|
||||
// Subagent frames ride the same stream as the conversation, but their uuids live on the
|
||||
// subagent's sidechain, so forkSession cannot resolve one. Anchoring a turn on one leaves
|
||||
// rewind throwing "Message <uuid> not found in session". The persisted history path already
|
||||
// skips sidechain entries; the live stream has to skip them too.
|
||||
if (readClaudeParentToolUseId(message)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
message.type === "user" &&
|
||||
!isSyntheticUserEntry(message) &&
|
||||
|
||||
Reference in New Issue
Block a user