mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 00:11:19 +00:00
fix: repair legacy message and route shapes at the sqlite import boundary
This commit is contained in:
parent
6bea48c671
commit
2fdeeeb515
5 changed files with 67 additions and 6 deletions
|
|
@ -1015,7 +1015,13 @@ function parseJsonlEntries(content: string): FileEntry[] {
|
|||
return entries;
|
||||
}
|
||||
|
||||
function normalizeLoadedFileEntry(entry: FileEntry): FileEntry {
|
||||
/**
|
||||
* Repairs legacy JSONL message shapes (string assistant/toolResult content) into
|
||||
* canonical block arrays. Every legacy-JSONL ingress point — file-era transcript
|
||||
* reads and the doctor SQLite import — must apply this so the SQLite store only
|
||||
* ever holds canonical rows; the SQLite read path does no repair.
|
||||
*/
|
||||
export function normalizeLoadedFileEntry(entry: FileEntry): FileEntry {
|
||||
if (!isJsonRecord(entry) || entry.type !== "message" || !isJsonRecord(entry.message)) {
|
||||
return entry;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import fs from "node:fs";
|
||||
import { TextDecoder } from "node:util";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeLoadedFileEntry, type FileEntry } from "../agents/sessions/session-manager.js";
|
||||
import type { TranscriptEvent } from "../config/sessions/session-accessor.js";
|
||||
import { resolveSqliteTargetFromSessionStorePath } from "../config/sessions/session-sqlite-target.js";
|
||||
import type { SessionStoreTarget } from "../config/sessions/targets.js";
|
||||
|
|
@ -77,7 +78,9 @@ export function createTranscriptEventReader(
|
|||
for (const line of iterateJsonlLinesSync(transcriptPath)) {
|
||||
const parsed = parseJsonlLine(line);
|
||||
if (parsed) {
|
||||
append(parsed as TranscriptEvent);
|
||||
// Import is the migration boundary: repair legacy JSONL message shapes
|
||||
// here because the SQLite runtime read path assumes canonical rows.
|
||||
append(normalizeLoadedFileEntry(parsed as FileEntry) as TranscriptEvent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -91,7 +94,7 @@ export function createTranscriptEventPrefixReader(
|
|||
for (const line of iterateJsonlLinesSync(transcriptPath)) {
|
||||
const parsed = parseJsonlLine(line);
|
||||
if (parsed) {
|
||||
append(parsed as TranscriptEvent);
|
||||
append(normalizeLoadedFileEntry(parsed as FileEntry) as TranscriptEvent);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -117,6 +117,44 @@ describe("runDoctorSessionSqlite", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("repairs legacy message and route shapes at the import boundary", async () => {
|
||||
const store = createLegacyStore({
|
||||
entryOverrides: {
|
||||
route: "stale-custom-slot",
|
||||
deliveryContext: { channel: "telegram", to: "123" },
|
||||
},
|
||||
transcriptLines: [
|
||||
'{"type":"session","sessionId":"session-1"}',
|
||||
'{"type":"message","id":"m1","parentId":null,"message":{"role":"assistant","content":"legacy string"}}',
|
||||
],
|
||||
});
|
||||
|
||||
const report = await runDoctorSessionSqlite({
|
||||
env: store.env,
|
||||
mode: "import",
|
||||
store: store.storePath,
|
||||
});
|
||||
|
||||
expect(report.totals).toMatchObject({ importedEntries: 1, issues: 0 });
|
||||
const imported = loadExactSqliteSessionEntry({
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
storePath: store.storePath,
|
||||
});
|
||||
// The SQLite runtime does no read repair, so import must store canonical shapes.
|
||||
expect(typeof imported?.entry.route).not.toBe("string");
|
||||
const events = loadSqliteTranscriptEventsSync({
|
||||
agentId: "main",
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:main",
|
||||
storePath: store.storePath,
|
||||
});
|
||||
const message = events.find((event) => (event as { type?: string }).type === "message") as {
|
||||
message?: { content?: unknown };
|
||||
};
|
||||
expect(message?.message?.content).toEqual([{ type: "text", text: "legacy string" }]);
|
||||
});
|
||||
|
||||
it("imports and validates legacy sessions idempotently", async () => {
|
||||
const store = createLegacyStore();
|
||||
|
||||
|
|
@ -1118,7 +1156,12 @@ describe("runDoctorSessionSqlite", () => {
|
|||
});
|
||||
|
||||
function createLegacyStore(
|
||||
params: { agentDirName?: string; customStore?: boolean; transcriptLines?: string[] } = {},
|
||||
params: {
|
||||
agentDirName?: string;
|
||||
customStore?: boolean;
|
||||
entryOverrides?: Record<string, unknown>;
|
||||
transcriptLines?: string[];
|
||||
} = {},
|
||||
): TestStore {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-session-sqlite-"));
|
||||
const stateDir = path.join(tempDir, "state");
|
||||
|
|
@ -1143,6 +1186,7 @@ function createLegacyStore(
|
|||
sessionId: "session-1",
|
||||
sessionStartedAt: 1000,
|
||||
updatedAt: 2000,
|
||||
...params.entryOverrides,
|
||||
},
|
||||
},
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { resolveSqliteTargetFromSessionStorePath } from "../config/sessions/session-sqlite-target.js";
|
||||
import { parseSqliteSessionFileMarker } from "../config/sessions/sqlite-marker.js";
|
||||
import { normalizeStoreSessionKey } from "../config/sessions/store-entry.js";
|
||||
import { normalizeSessionEntryDelivery } from "../config/sessions/store-load.js";
|
||||
import {
|
||||
resolveAgentSessionStoreTargetsSync,
|
||||
resolveAllAgentSessionStoreTargetsSync,
|
||||
|
|
@ -323,7 +324,9 @@ function readLegacySessionRecords(
|
|||
continue;
|
||||
}
|
||||
records.push({
|
||||
entry: value,
|
||||
// Import is the migration boundary: repair legacy delivery/route shapes
|
||||
// here because the SQLite runtime read path assumes canonical entries.
|
||||
entry: normalizeSessionEntryDelivery(value),
|
||||
sessionKey,
|
||||
transcriptPath: resolveLegacyTranscriptPath(target, value),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -296,7 +296,12 @@ function sameDeliveryChannelRoute(
|
|||
);
|
||||
}
|
||||
|
||||
function normalizeSessionEntryDelivery(entry: SessionEntry): SessionEntry {
|
||||
/**
|
||||
* Rebuilds malformed/legacy delivery `route` state from the entry's delivery
|
||||
* fields. Runs on file-era store loads and on the doctor SQLite import so the
|
||||
* SQLite store only holds canonical delivery shapes; SQLite reads do no repair.
|
||||
*/
|
||||
export function normalizeSessionEntryDelivery(entry: SessionEntry): SessionEntry {
|
||||
const entryRoute = normalizeDeliveryChannelRoute(entry.route);
|
||||
const normalized = normalizeSessionDeliveryFields({
|
||||
route: entryRoute,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue