fix(gateway): preserve partial transcript branches

This commit is contained in:
Vincent Koc
2026-05-16 13:55:24 +08:00
parent ae42768e5f
commit 3bc7d4061b
3 changed files with 73 additions and 4 deletions

View File

@@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai
- Config persistence: strip malformed pending final-delivery session fields on load so replay/recovery paths skip poisoned reply metadata instead of crashing on raw objects.
- Config persistence: strip malformed plugin extension state and promoted session-slot ownership on load so corrupted session rows do not leak poisoned plugin metadata into replay/projection paths.
- Gateway/sessions: ignore malformed compaction checkpoint rows during session projection so corrupted stores do not crash session list/describe responses or show bogus checkpoint counts.
- Gateway/sessions: keep reachable transcript history when imported tree transcripts reference missing or legacy parent rows, preventing session history reads from going empty after a partial import.
- Providers: reject malformed successful Runway, BytePlus, and Ollama embedding responses with provider-owned errors instead of raw parser/type failures, silent bad vectors, or long bogus polling.
- Providers/images: reject malformed successful OpenAI-compatible, OpenAI, Google, fal, and OpenRouter image responses with provider-owned errors instead of raw shape failures, silent invalid base64 skips, or empty image results.
- Providers/videos: reject malformed successful xAI, OpenRouter, and fal video create, poll, and result responses with provider-owned errors instead of raw parser failures or long bogus polling.

View File

@@ -146,7 +146,7 @@ function buildActiveTreeEntries(params: {
seen.add(currentId);
const entry = params.byId.get(currentId);
if (!entry) {
return [];
break;
}
out.push(entry);
currentId = entry.parentId ?? undefined;
@@ -206,10 +206,12 @@ async function buildSessionTranscriptIndex(
record: parsed,
};
rawEntries.push(rawEntry);
if (isTreeTranscriptRecord(parsed) && id) {
hasTreeEntries = true;
leafId = id;
if (id) {
byId.set(id, rawEntry);
if (isTreeTranscriptRecord(parsed)) {
hasTreeEntries = true;
leafId = id;
}
}
});

View File

@@ -886,6 +886,72 @@ describe("readSessionMessages", () => {
}
});
test("keeps async active branch rows when imported parent links are incomplete", async () => {
const sessionId = "test-session-tree-async-incomplete-parent";
writeTranscript(tmpDir, sessionId, [
{ type: "session", version: 3, id: sessionId },
{
type: "message",
id: "legacy-user",
message: { role: "user", content: "legacy prompt" },
},
{
type: "message",
id: "tree-assistant",
parentId: "legacy-user",
message: { role: "assistant", content: "tree reply" },
},
{
type: "message",
id: "orphan-tail",
parentId: "missing-imported-parent",
message: { role: "assistant", content: "reachable orphan tail" },
},
]);
clearSessionTranscriptIndexCache();
const messages = await readSessionMessagesAsync(sessionId, storePath, undefined, {
mode: "full",
reason: "test imported partial tree selection",
});
expect(messages.map((message) => (message as { content?: unknown }).content)).toEqual([
"reachable orphan tail",
]);
expectMessageFields(messages[0], { openclaw: { id: "orphan-tail", seq: 1 } });
});
test("keeps legacy async parents when tree transcripts reference pre-v3 rows", async () => {
const sessionId = "test-session-tree-async-legacy-parent";
writeTranscript(tmpDir, sessionId, [
{ type: "session", version: 1, id: sessionId },
{
type: "message",
id: "legacy-user",
message: { role: "user", content: "legacy hello" },
},
{
type: "message",
id: "tree-assistant",
parentId: "legacy-user",
message: { role: "assistant", content: "tree hello" },
},
]);
clearSessionTranscriptIndexCache();
const messages = await readSessionMessagesAsync(sessionId, storePath, undefined, {
mode: "full",
reason: "test legacy parent active tree selection",
});
expect(messages.map((message) => (message as { content?: unknown }).content)).toEqual([
"legacy hello",
"tree hello",
]);
expectMessageFields(messages[0], { openclaw: { id: "legacy-user", seq: 1 } });
expectMessageFields(messages[1], { openclaw: { id: "tree-assistant", seq: 2 } });
});
test("caches async transcript indexes by file stats", async () => {
const sessionId = "test-session-index-cache";
writeTranscript(tmpDir, sessionId, [