fix: preserve sqlite migration compatibility

This commit is contained in:
Peter Steinberger
2026-05-16 01:26:33 +01:00
parent 7a7ae015a9
commit 3e10843e23
7 changed files with 173 additions and 6 deletions

View File

@@ -961,6 +961,30 @@ describe("active-memory plugin", () => {
);
});
it("keeps legacy dm session keys eligible for direct recall", async () => {
api.pluginConfig = {
agents: ["main"],
allowedChatTypes: ["direct"],
};
plugin.register(api as unknown as OpenClawPluginApi);
const result = await hooks.before_prompt_build(
{ prompt: "what did we discuss?", messages: [] },
{
agentId: "main",
trigger: "user",
sessionKey: "agent:main:dm:peer-123",
messageProvider: "telegram",
channelId: "telegram",
},
);
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
expect(result).toEqual({
prependContext: expect.stringContaining("<active_memory_plugin>"),
});
});
it("uses messageProvider not Google Chat space id for embedded recall (#78918)", async () => {
seedSessionEntry("agent:main:googlechat:default:direct:spaces/khfx4yaaaae", {
chatType: "direct",
@@ -1020,6 +1044,30 @@ describe("active-memory plugin", () => {
});
});
it("keeps exact explicit session keys eligible when no session row exists yet", async () => {
api.pluginConfig = {
agents: ["main"],
allowedChatTypes: ["explicit"],
};
plugin.register(api as unknown as OpenClawPluginApi);
const result = await hooks.before_prompt_build(
{ prompt: "what should i work on next?", messages: [] },
{
agentId: "main",
trigger: "user",
sessionKey: "agent:main:explicit",
messageProvider: "custom",
channelId: "custom",
},
);
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
expect(result).toEqual({
prependContext: expect.stringContaining("<active_memory_plugin>"),
});
});
it("keeps explicit session classification when the opaque session id contains chat-type tokens", async () => {
seedSessionEntry("agent:main:explicit:portal-123:group:shadow", { chatType: "explicit" });
api.pluginConfig = {

View File

@@ -1,5 +1,5 @@
import crypto from "node:crypto";
import { loadSqliteSessionTranscriptEvents } from "openclaw/plugin-sdk/agent-harness-runtime";
import { loadSqliteSessionTranscriptBoundedEvents } from "openclaw/plugin-sdk/agent-harness-runtime";
import {
DEFAULT_PROVIDER,
parseModelRef,
@@ -1105,6 +1105,9 @@ function resolveChatType(ctx: {
if (sessionKey.includes(":direct:")) {
return "direct";
}
if (sessionKey.includes(":dm:")) {
return "direct";
}
if (sessionKey.includes(":group:")) {
return "group";
}
@@ -1114,6 +1117,9 @@ function resolveChatType(ctx: {
if (sessionKey.includes(":explicit:")) {
return "explicit";
}
if (/^agent:[^:]+:explicit$/.test(sessionKey)) {
return "explicit";
}
if (/^agent:[^:]+:main:thread:/.test(sessionKey)) {
return "direct";
}
@@ -1537,11 +1543,12 @@ async function streamBoundedTranscriptEvents(params: {
}): Promise<void> {
const limits = resolveTranscriptReadLimits(params.limits);
try {
const events = loadSqliteSessionTranscriptEvents(params.transcriptScope);
if (JSON.stringify(events.map((entry) => entry.event)).length > limits.maxBytes) {
return;
}
for (const { event } of events.slice(0, limits.maxLines)) {
const events = loadSqliteSessionTranscriptBoundedEvents({
...params.transcriptScope,
maxBytes: limits.maxBytes,
maxEvents: limits.maxLines,
});
for (const { event } of events) {
if (params.onRecord(event)) {
break;
}

View File

@@ -203,6 +203,20 @@ describe("doctor session transcript repair", () => {
threadId: "thread-123",
cwd: root,
model: "gpt-5.5",
userMcpServersFingerprint: "user-mcp-v1",
mcpServersFingerprint: "mcp-v1",
pluginAppsFingerprint: "plugin-apps-v1",
pluginAppsInputFingerprint: "plugin-app-input-v1",
pluginAppPolicyContext: {
fingerprint: "policy-v1",
apps: {},
pluginAppIds: {},
},
contextEngine: {
schemaVersion: 1,
engineId: "context-engine",
policyFingerprint: "context-policy-v1",
},
}),
);
@@ -220,6 +234,20 @@ describe("doctor session transcript repair", () => {
sessionId: "session-1",
cwd: root,
model: "gpt-5.5",
userMcpServersFingerprint: "user-mcp-v1",
mcpServersFingerprint: "mcp-v1",
pluginAppsFingerprint: "plugin-apps-v1",
pluginAppsInputFingerprint: "plugin-app-input-v1",
pluginAppPolicyContext: {
fingerprint: "policy-v1",
apps: {},
pluginAppIds: {},
},
contextEngine: {
schemaVersion: 1,
engineId: "context-engine",
policyFingerprint: "context-policy-v1",
},
});
const [message, title] = note.mock.calls[0] as [string, string];
expect(title).toBe("Session transcripts");

View File

@@ -332,7 +332,9 @@ function normalizeCodexAppServerBindingPayload(
) {
return undefined;
}
const payload = JSON.parse(JSON.stringify(parsed)) as Record<string, unknown>;
return {
...payload,
schemaVersion: 1,
sessionId,
threadId: parsed.threadId,

View File

@@ -19,6 +19,7 @@ import {
countSqliteSessionTranscriptDisplayMessages,
deleteSqliteSessionTranscript,
listSqliteSessionTranscripts,
loadSqliteSessionTranscriptBoundedEvents,
loadSqliteSessionTranscriptEvents,
loadSqliteSessionTranscriptTailEvents,
recordSqliteSessionTranscriptSnapshot,
@@ -290,6 +291,45 @@ describe("SQLite session transcript store", () => {
).toBe(8);
});
it("reads bounded transcript heads without materializing rows beyond caps", () => {
const stateDir = createTempDir();
const env = { OPENCLAW_STATE_DIR: stateDir };
replaceSqliteSessionTranscriptEvents({
env,
agentId: "main",
sessionId: "session-1",
events: [
{ type: "session", id: "session-1" },
{ type: "message", id: "m1", message: { role: "assistant", content: "short" } },
{
type: "message",
id: "m2",
message: { role: "assistant", content: "this row should not be parsed" },
},
],
now: () => 100,
});
expect(
loadSqliteSessionTranscriptBoundedEvents({
env,
agentId: "main",
sessionId: "session-1",
maxEvents: 2,
maxBytes: 120,
}).map((entry) => (entry.event as { id?: string }).id),
).toEqual(["session-1", "m1"]);
expect(
loadSqliteSessionTranscriptBoundedEvents({
env,
agentId: "main",
sessionId: "session-1",
maxEvents: 3,
maxBytes: 8,
}),
).toEqual([]);
});
it("preserves event timestamps when replacing transcript rows", () => {
const stateDir = createTempDir();
const env = { OPENCLAW_STATE_DIR: stateDir };

View File

@@ -51,6 +51,12 @@ export type LoadSqliteSessionTranscriptTailEventsOptions = SqliteSessionTranscri
maxEvents: number;
};
export type LoadSqliteSessionTranscriptBoundedEventsOptions =
SqliteSessionTranscriptStoreOptions & {
maxBytes?: number;
maxEvents: number;
};
export type SqliteSessionTranscriptScope = {
agentId: string;
path?: string;
@@ -738,6 +744,41 @@ export function loadSqliteSessionTranscriptTailEvents(
return selected.toReversed().map(parseTranscriptEventRow);
}
export function loadSqliteSessionTranscriptBoundedEvents(
options: LoadSqliteSessionTranscriptBoundedEventsOptions,
): SqliteSessionTranscriptEvent[] {
const { sessionId } = normalizeTranscriptScope(options);
const database = openTranscriptAgentDatabase(options);
const maxEvents = normalizePositiveInteger(options.maxEvents, 1);
const maxBytes =
typeof options.maxBytes === "number" && Number.isFinite(options.maxBytes)
? Math.max(1, Math.floor(options.maxBytes))
: undefined;
const rows = executeSqliteQuerySync(
database.db,
getAgentTranscriptKysely(database.db)
.selectFrom("transcript_events")
.select(["seq", "event_json", "created_at"])
.where("session_id", "=", sessionId)
.orderBy("seq", "asc")
.limit(maxEvents),
).rows;
const selected: typeof rows = [];
let bytes = 0;
for (const row of rows) {
const eventBytes = Buffer.byteLength(row.event_json, "utf8") + 1;
if (maxBytes !== undefined && selected.length > 0 && bytes + eventBytes > maxBytes) {
break;
}
if (maxBytes !== undefined && selected.length === 0 && eventBytes > maxBytes) {
return [];
}
selected.push(row);
bytes += eventBytes;
}
return selected.map(parseTranscriptEventRow);
}
export function countSqliteSessionTranscriptDisplayMessages(
options: SqliteSessionTranscriptStoreOptions,
): number {

View File

@@ -36,6 +36,7 @@ export { canonicalizeMainSessionAlias } from "../config/sessions/main-session.js
export {
appendSqliteSessionTranscriptEvent,
hasSqliteSessionTranscriptEvents,
loadSqliteSessionTranscriptBoundedEvents,
loadSqliteSessionTranscriptEvents,
replaceSqliteSessionTranscriptEvents,
} from "../config/sessions/transcript-store.sqlite.js";