mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-21 05:32:53 +00:00
fix: preserve legacy replay phase boundaries (#61529) (thanks @100yenadmin)
This commit is contained in:
@@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Docs/i18n: relocalize final localized-page links after translation so generated locale pages stop keeping stale English-root links when targets appear later in the same run. (#61796) thanks @hxy91819.
|
||||
- iOS/Watch exec approvals: keep Apple Watch review and approval recovery working while the iPhone is locked or backgrounded, including background-safe reconnects, persisted pending approvals, notification cleanup, and APNs-backed watch refresh recovery. (#61757) Thanks @ngutman.
|
||||
- Gateway/status: probe local TLS gateways over `wss://`, forward the local cert fingerprint for self-signed loopback probes, and warn when the local TLS runtime cannot load the configured cert. (#61935) Thanks @ThanhNguyxn07.
|
||||
- Agents/history: keep truly legacy unsigned replay text unphased when mixed with phased OpenAI WS assistant blocks, while still inheriting message phase for id-only replay signatures. (#61529) Thanks @100yenadmin.
|
||||
|
||||
## 2026.4.5
|
||||
|
||||
|
||||
@@ -329,6 +329,16 @@ export function convertMessagesToInputItems(
|
||||
if (Array.isArray(content)) {
|
||||
const textParts: string[] = [];
|
||||
let currentTextPhase: OpenAIResponsesAssistantPhase | undefined;
|
||||
const hasExplicitBlockPhase = content.some((block) => {
|
||||
if (!block || typeof block !== "object") {
|
||||
return false;
|
||||
}
|
||||
const record = block as { type?: unknown; textSignature?: unknown };
|
||||
return (
|
||||
record.type === "text" &&
|
||||
Boolean(parseAssistantTextSignature(record.textSignature)?.phase)
|
||||
);
|
||||
});
|
||||
const pushAssistantText = (phase?: OpenAIResponsesAssistantPhase) => {
|
||||
if (textParts.length === 0) {
|
||||
return;
|
||||
@@ -354,7 +364,12 @@ export function convertMessagesToInputItems(
|
||||
if (block.type === "text" && typeof block.text === "string") {
|
||||
const parsedSignature = parseAssistantTextSignature(block.textSignature);
|
||||
const blockPhase =
|
||||
parsedSignature?.phase ?? assistantMessagePhase;
|
||||
parsedSignature?.phase ??
|
||||
(parsedSignature?.id
|
||||
? assistantMessagePhase
|
||||
: hasExplicitBlockPhase
|
||||
? undefined
|
||||
: assistantMessagePhase);
|
||||
if (textParts.length > 0 && blockPhase !== currentTextPhase) {
|
||||
pushAssistantText(currentTextPhase);
|
||||
}
|
||||
|
||||
@@ -619,7 +619,7 @@ describe("convertMessagesToInputItems", () => {
|
||||
[{ id: "call_1", name: "exec", args: { cmd: "ls" } }],
|
||||
"commentary",
|
||||
);
|
||||
const items = convertMessagesToInputItems([msg] as Parameters<
|
||||
const items = convertMessagesToInputItems([msg] as unknown as Parameters<
|
||||
typeof convertMessagesToInputItems
|
||||
>[0]);
|
||||
const textItem = items.find((i) => i.type === "message");
|
||||
@@ -648,7 +648,7 @@ describe("convertMessagesToInputItems", () => {
|
||||
usage: {},
|
||||
timestamp: 0,
|
||||
};
|
||||
const items = convertMessagesToInputItems([msg] as Parameters<
|
||||
const items = convertMessagesToInputItems([msg] as unknown as Parameters<
|
||||
typeof convertMessagesToInputItems
|
||||
>[0]);
|
||||
expect(items).toHaveLength(1);
|
||||
@@ -704,7 +704,45 @@ describe("convertMessagesToInputItems", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("inherits message-level phase for untagged blocks, merging with phased text", () => {
|
||||
it("inherits message-level phase for id-only textSignature blocks, merging with phased text", () => {
|
||||
const msg = {
|
||||
role: "assistant" as const,
|
||||
phase: "final_answer" as const,
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: "Replay. ",
|
||||
textSignature: JSON.stringify({ v: 1, id: "item_pending_phase" }),
|
||||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
text: "Done.",
|
||||
textSignature: JSON.stringify({ v: 1, id: "item_final", phase: "final_answer" }),
|
||||
},
|
||||
],
|
||||
stopReason: "stop",
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
model: "gpt-5.2",
|
||||
usage: {},
|
||||
timestamp: 0,
|
||||
};
|
||||
|
||||
expect(
|
||||
convertMessagesToInputItems([msg] as unknown as Parameters<
|
||||
typeof convertMessagesToInputItems
|
||||
>[0]),
|
||||
).toEqual([
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: "Replay. Done.",
|
||||
phase: "final_answer",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps truly unsigned legacy blocks separate when phased siblings are present", () => {
|
||||
const msg = {
|
||||
role: "assistant" as const,
|
||||
phase: "final_answer" as const,
|
||||
@@ -735,10 +773,12 @@ describe("convertMessagesToInputItems", () => {
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
// Both blocks share the same effective phase (final_answer):
|
||||
// the untagged block now inherits message-level phase instead
|
||||
// of being forced to undefined (#61476).
|
||||
content: "Legacy. Done.",
|
||||
content: "Legacy. ",
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: "Done.",
|
||||
phase: "final_answer",
|
||||
},
|
||||
]);
|
||||
@@ -868,7 +908,7 @@ describe("convertMessagesToInputItems", () => {
|
||||
|
||||
it("handles assistant messages with only tool calls (no text)", () => {
|
||||
const msg = assistantMsg([], [{ id: "call_2", name: "read", args: { path: "/etc/hosts" } }]);
|
||||
const items = convertMessagesToInputItems([msg] as Parameters<
|
||||
const items = convertMessagesToInputItems([msg] as unknown as Parameters<
|
||||
typeof convertMessagesToInputItems
|
||||
>[0]);
|
||||
expect(items).toHaveLength(1);
|
||||
@@ -3163,12 +3203,17 @@ describe("releaseWsSession / hasWsSession", () => {
|
||||
});
|
||||
|
||||
describe("convertMessagesToInputItems — phase inheritance", () => {
|
||||
it("untagged text blocks inherit message-level phase when siblings have explicit textSignature", () => {
|
||||
it("keeps unsigned legacy text unphased while id-only replay text inherits message phase", () => {
|
||||
const msg = {
|
||||
role: "assistant" as const,
|
||||
phase: "commentary",
|
||||
content: [
|
||||
{ type: "text", text: "Untagged block A" },
|
||||
{
|
||||
type: "text",
|
||||
text: "Replay block",
|
||||
textSignature: JSON.stringify({ v: 1, id: "s0" }),
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "Explicitly final",
|
||||
@@ -3177,15 +3222,30 @@ describe("convertMessagesToInputItems — phase inheritance", () => {
|
||||
{ type: "text", text: "Untagged block B" },
|
||||
],
|
||||
};
|
||||
const items = convertMessagesToInputItems([msg] as Parameters<
|
||||
const items = convertMessagesToInputItems([msg] as unknown as Parameters<
|
||||
typeof convertMessagesToInputItems
|
||||
>[0]);
|
||||
const assistantItems = items.filter((i: Record<string, unknown>) => i.role === "assistant");
|
||||
// Should produce 3 separate assistant items because phase changes:
|
||||
// A=commentary, Explicit=final_answer, B=commentary
|
||||
expect(assistantItems).toHaveLength(3);
|
||||
expect((assistantItems[0] as Record<string, unknown>).phase).toBe("commentary");
|
||||
expect((assistantItems[1] as Record<string, unknown>).phase).toBe("final_answer");
|
||||
expect((assistantItems[2] as Record<string, unknown>).phase).toBe("commentary");
|
||||
expect(assistantItems).toHaveLength(4);
|
||||
expect(assistantItems[0]).toMatchObject({
|
||||
role: "assistant",
|
||||
content: "Untagged block A",
|
||||
});
|
||||
expect((assistantItems[0] as Record<string, unknown>).phase).toBeUndefined();
|
||||
expect(assistantItems[1]).toMatchObject({
|
||||
role: "assistant",
|
||||
content: "Replay block",
|
||||
phase: "commentary",
|
||||
});
|
||||
expect(assistantItems[2]).toMatchObject({
|
||||
role: "assistant",
|
||||
content: "Explicitly final",
|
||||
phase: "final_answer",
|
||||
});
|
||||
expect(assistantItems[3]).toMatchObject({
|
||||
role: "assistant",
|
||||
content: "Untagged block B",
|
||||
});
|
||||
expect((assistantItems[3] as Record<string, unknown>).phase).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user