fix: preserve source agent for sqlite session rows

This commit is contained in:
Peter Steinberger
2026-05-17 15:44:10 +01:00
parent 9c06cb6f82
commit 6bf4e35768
4 changed files with 117 additions and 11 deletions

View File

@@ -16,6 +16,7 @@ function mergeSessionEntryIntoCombined(params: {
cfg: OpenClawConfig;
combined: Record<string, SessionEntry>;
sourceDatabasePathBySessionKey: Record<string, string>;
sourceAgentIdBySessionKey: Record<string, string>;
sourceDatabasePath: string;
entry: SessionEntry;
agentId: string;
@@ -53,6 +54,7 @@ function mergeSessionEntryIntoCombined(params: {
};
}
params.sourceDatabasePathBySessionKey[canonicalKey] = params.sourceDatabasePath;
params.sourceAgentIdBySessionKey[canonicalKey] = agentId;
}
export function loadCombinedSessionEntriesForGateway(
@@ -62,6 +64,7 @@ export function loadCombinedSessionEntriesForGateway(
databasePath: string;
entries: Record<string, SessionEntry>;
sourceDatabasePathBySessionKey?: Record<string, string>;
sourceAgentIdBySessionKey?: Record<string, string>;
} {
const requestedAgentId =
typeof opts.agentId === "string" && opts.agentId.trim()
@@ -76,6 +79,7 @@ export function loadCombinedSessionEntriesForGateway(
: resolveAllAgentSessionDatabaseTargetsSync(cfg);
const combined: Record<string, SessionEntry> = {};
const sourceDatabasePathBySessionKey: Record<string, string> = {};
const sourceAgentIdBySessionKey: Record<string, string> = {};
for (const target of targets) {
const agentId = target.agentId;
for (const { sessionKey: key, entry } of listSessionEntries({
@@ -91,6 +95,7 @@ export function loadCombinedSessionEntriesForGateway(
cfg,
combined,
sourceDatabasePathBySessionKey,
sourceAgentIdBySessionKey,
sourceDatabasePath: target.databasePath,
entry,
agentId,
@@ -105,5 +110,10 @@ export function loadCombinedSessionEntriesForGateway(
: targets.length === 1
? targets[0].databasePath
: "(multiple)";
return { databasePath, entries: combined, sourceDatabasePathBySessionKey };
return {
databasePath,
entries: combined,
sourceDatabasePathBySessionKey,
sourceAgentIdBySessionKey,
};
}

View File

@@ -901,6 +901,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
databasePath,
entries: store,
sourceDatabasePathBySessionKey,
sourceAgentIdBySessionKey,
} = measureDiagnosticsTimelineSpanSync(
"gateway.sessions.list.store_load",
() => {
@@ -938,6 +939,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
cfg,
databasePath,
sourceDatabasePathBySessionKey,
sourceAgentIdBySessionKey,
store,
modelCatalog,
opts: p,

View File

@@ -8,10 +8,14 @@ import type { OpenClawConfig } from "../config/config.js";
import { getSessionEntry, upsertSessionEntry, type SessionEntry } from "../config/sessions.js";
import { replaceSqliteSessionTranscriptEvents } from "../config/sessions/transcript-store.sqlite.js";
import { registerAgentRunContext, resetAgentRunContextForTest } from "../infra/agent-events.js";
import { openOpenClawAgentDatabase } from "../state/openclaw-agent-db.js";
import {
listOpenClawRegisteredAgentDatabases,
openOpenClawAgentDatabase,
} from "../state/openclaw-agent-db.js";
import { withStateDirEnv } from "../test-helpers/state-dir-env.js";
import {
listSessionsFromStore,
listSessionsFromStoreAsync,
loadCombinedSessionEntriesForGateway,
resolveGatewayModelSupportsImages,
} from "./session-utils.js";
@@ -1219,6 +1223,7 @@ describe("loadCombinedSessionEntriesForGateway includes SQLite-registered agents
cfg,
databasePath: combined.databasePath,
sourceDatabasePathBySessionKey: combined.sourceDatabasePathBySessionKey,
sourceAgentIdBySessionKey: combined.sourceAgentIdBySessionKey,
store: combined.entries,
opts: { agentId: "work", includeDerivedTitles: true, includeLastMessage: true },
});
@@ -1233,6 +1238,67 @@ describe("loadCombinedSessionEntriesForGateway includes SQLite-registered agents
});
});
test("global rows from registered agent databases preserve their source agent", async () => {
await withStateDirEnv("openclaw-acp-registered-global-owner-", async ({ tempRoot }) => {
const databasePath = path.join(tempRoot, "relocated", "work.sqlite");
openOpenClawAgentDatabase({ agentId: "work", path: databasePath });
upsertSessionEntry({
agentId: "work",
path: databasePath,
sessionKey: "global",
entry: {
sessionId: "s-work-global",
updatedAt: 400,
modelProvider: "openai",
model: "gpt-5.4",
},
});
replaceSqliteSessionTranscriptEvents({
agentId: "work",
path: databasePath,
sessionId: "s-work-global",
events: [
{ type: "session", version: 1, id: "s-work-global" },
{ message: { role: "user", content: "work global title" } },
{ message: { role: "assistant", content: "work global last" } },
],
});
const cfg = {
session: {
mainKey: "main",
},
agents: {
list: [{ id: "main", default: true }],
},
} as OpenClawConfig;
const combined = loadCombinedSessionEntriesForGateway(cfg, { agentId: "work" });
expect(combined.entries.global?.sessionId).toBe("s-work-global");
expect(combined.sourceAgentIdBySessionKey?.global).toBe("work");
const result = await listSessionsFromStoreAsync({
cfg,
databasePath: combined.databasePath,
sourceDatabasePathBySessionKey: combined.sourceDatabasePathBySessionKey,
sourceAgentIdBySessionKey: combined.sourceAgentIdBySessionKey,
store: combined.entries,
opts: { includeGlobal: true, includeDerivedTitles: true, includeLastMessage: true },
});
expect(result.sessions[0]).toEqual(
expect.objectContaining({
key: "global",
derivedTitle: "work global title",
lastMessagePreview: "work global last",
}),
);
expect(
listOpenClawRegisteredAgentDatabases()
.filter((row) => row.path === databasePath)
.map((row) => row.agentId),
).toEqual(["work"]);
});
});
test("configured-only loads preserve registered database paths", async () => {
await withStateDirEnv("openclaw-acp-configured-registered-path-", async ({ tempRoot }) => {
const databasePath = path.join(tempRoot, "relocated", "work.sqlite");

View File

@@ -670,6 +670,7 @@ function resolveChildSessionKeys(
function resolveTranscriptUsageFallback(params: {
cfg: OpenClawConfig;
agentId?: string;
databasePath?: string;
key: string;
entry?: SessionEntry;
@@ -690,9 +691,9 @@ function resolveTranscriptUsageFallback(params: {
return null;
}
const parsed = parseAgentSessionKey(params.key);
const agentId = parsed?.agentId
? normalizeAgentId(parsed.agentId)
: resolveDefaultAgentId(params.cfg);
const agentId = normalizeAgentId(
params.agentId ?? parsed?.agentId ?? resolveDefaultAgentId(params.cfg),
);
const snapshot = readRecentSessionUsageFromTranscript(
{
agentId,
@@ -1305,6 +1306,7 @@ export function resolveSessionDisplayModelIdentityRef(params: {
export function buildGatewaySessionRow(params: {
cfg: OpenClawConfig;
agentId?: string;
databasePath?: string;
store: Record<string, SessionEntry>;
key: string;
@@ -1347,7 +1349,9 @@ export function buildGatewaySessionRow(params: {
deliveryContext: entry?.deliveryContext,
});
const parsedAgent = parseAgentSessionKey(key);
const sessionAgentId = normalizeAgentId(parsedAgent?.agentId ?? resolveDefaultAgentId(cfg));
const sessionAgentId = normalizeAgentId(
params.agentId ?? parsedAgent?.agentId ?? resolveDefaultAgentId(cfg),
);
const rowContext = params.rowContext;
const subagentRun = rowContext
? rowContext.subagentRuns.getDisplaySubagentRun(key)
@@ -1433,6 +1437,7 @@ export function buildGatewaySessionRow(params: {
(needsTranscriptTotalTokens || needsTranscriptContextTokens || needsTranscriptEstimatedCostUsd)
? resolveTranscriptUsageFallback({
cfg,
agentId: sessionAgentId,
databasePath: params.databasePath,
key,
entry,
@@ -1957,10 +1962,24 @@ function resolveSessionRowSourceDatabasePath(params: {
return databasePath && databasePath !== "(multiple)" ? databasePath : undefined;
}
function resolveSessionRowSourceAgentId(params: {
cfg: OpenClawConfig;
sourceAgentIdBySessionKey?: Record<string, string>;
key: string;
}): string {
const parsed = parseAgentSessionKey(params.key);
return normalizeAgentId(
params.sourceAgentIdBySessionKey?.[params.key] ??
parsed?.agentId ??
resolveDefaultAgentId(params.cfg),
);
}
export function listSessionsFromStore(params: {
cfg: OpenClawConfig;
databasePath?: string;
sourceDatabasePathBySessionKey?: Record<string, string>;
sourceAgentIdBySessionKey?: Record<string, string>;
store: Record<string, SessionEntry>;
modelCatalog?: ModelCatalogEntry[];
opts: import("./protocol/index.js").SessionsListParams;
@@ -1998,8 +2017,14 @@ export function listSessionsFromStore(params: {
sourceDatabasePathBySessionKey: params.sourceDatabasePathBySessionKey,
key,
});
const rowAgentId = resolveSessionRowSourceAgentId({
cfg,
sourceAgentIdBySessionKey: params.sourceAgentIdBySessionKey,
key,
});
return buildGatewaySessionRow({
cfg,
agentId: rowAgentId,
databasePath: rowDatabasePath,
store,
key,
@@ -2044,6 +2069,7 @@ export async function listSessionsFromStoreAsync(params: {
cfg: OpenClawConfig;
databasePath?: string;
sourceDatabasePathBySessionKey?: Record<string, string>;
sourceAgentIdBySessionKey?: Record<string, string>;
store: Record<string, SessionEntry>;
modelCatalog?: ModelCatalogEntry[];
opts: import("./protocol/index.js").SessionsListParams;
@@ -2083,8 +2109,14 @@ export async function listSessionsFromStoreAsync(params: {
sourceDatabasePathBySessionKey: params.sourceDatabasePathBySessionKey,
key,
});
const rowAgentId = resolveSessionRowSourceAgentId({
cfg,
sourceAgentIdBySessionKey: params.sourceAgentIdBySessionKey,
key,
});
const row = buildGatewaySessionRow({
cfg,
agentId: rowAgentId,
databasePath: rowDatabasePath,
store,
key,
@@ -2104,12 +2136,8 @@ export async function listSessionsFromStoreAsync(params: {
includeTranscriptFields &&
(includeDerivedTitles || includeLastMessage)
) {
const parsed = parseAgentSessionKey(key);
const sessionAgentId = parsed?.agentId
? normalizeAgentId(parsed.agentId)
: resolveDefaultAgentId(cfg);
const fields = await readSessionTitleFieldsFromTranscriptAsync({
agentId: sessionAgentId,
agentId: rowAgentId,
...(rowDatabasePath ? { path: rowDatabasePath } : {}),
sessionId: entry.sessionId,
});