mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 03:43:54 +00:00
fix: preserve sqlite migration compatibility
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user