From 690bea5e3dfc79ad79e88e343c007d75db02b4cc Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Thu, 12 Mar 2026 01:47:40 +0000 Subject: [PATCH] ACP: resolve diagnostics from configured session stores --- src/acp/runtime/session-meta.test.ts | 69 ++++++++++++++++++ src/acp/runtime/session-meta.ts | 17 ++--- src/commands/session-store-targets.test.ts | 4 +- src/commands/session-store-targets.ts | 83 ++-------------------- src/config/sessions.ts | 1 + src/config/sessions/targets.ts | 80 +++++++++++++++++++++ 6 files changed, 165 insertions(+), 89 deletions(-) create mode 100644 src/acp/runtime/session-meta.test.ts create mode 100644 src/config/sessions/targets.ts diff --git a/src/acp/runtime/session-meta.test.ts b/src/acp/runtime/session-meta.test.ts new file mode 100644 index 00000000000..71b27924082 --- /dev/null +++ b/src/acp/runtime/session-meta.test.ts @@ -0,0 +1,69 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../config/config.js"; + +const hoisted = vi.hoisted(() => { + const resolveSessionStoreTargetsMock = vi.fn(); + const loadSessionStoreMock = vi.fn(); + return { + resolveSessionStoreTargetsMock, + loadSessionStoreMock, + }; +}); + +vi.mock("../../config/sessions.js", async () => { + const actual = await vi.importActual( + "../../config/sessions.js", + ); + return { + ...actual, + resolveSessionStoreTargets: (cfg: OpenClawConfig, opts: unknown) => + hoisted.resolveSessionStoreTargetsMock(cfg, opts), + loadSessionStore: (storePath: string) => hoisted.loadSessionStoreMock(storePath), + }; +}); + +const { listAcpSessionEntries } = await import("./session-meta.js"); + +describe("listAcpSessionEntries", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("reads ACP sessions from resolved configured store targets", async () => { + const cfg = { + session: { + store: "/custom/sessions/{agentId}.json", + }, + } as OpenClawConfig; + hoisted.resolveSessionStoreTargetsMock.mockReturnValue([ + { + agentId: "ops", + storePath: "/custom/sessions/ops.json", + }, + ]); + hoisted.loadSessionStoreMock.mockReturnValue({ + "agent:ops:acp:s1": { + updatedAt: 123, + acp: { + backend: "acpx", + agent: "ops", + mode: "persistent", + state: "idle", + }, + }, + }); + + const entries = await listAcpSessionEntries({ cfg }); + + expect(hoisted.resolveSessionStoreTargetsMock).toHaveBeenCalledWith(cfg, { allAgents: true }); + expect(hoisted.loadSessionStoreMock).toHaveBeenCalledWith("/custom/sessions/ops.json"); + expect(entries).toEqual([ + expect.objectContaining({ + cfg, + storePath: "/custom/sessions/ops.json", + sessionKey: "agent:ops:acp:s1", + storeSessionKey: "agent:ops:acp:s1", + }), + ]); + }); +}); diff --git a/src/acp/runtime/session-meta.ts b/src/acp/runtime/session-meta.ts index fd4a5813f9b..34e84832c4f 100644 --- a/src/acp/runtime/session-meta.ts +++ b/src/acp/runtime/session-meta.ts @@ -1,9 +1,11 @@ -import path from "node:path"; -import { resolveAgentSessionDirs } from "../../agents/session-dirs.js"; import type { OpenClawConfig } from "../../config/config.js"; import { loadConfig } from "../../config/config.js"; -import { resolveStateDir } from "../../config/paths.js"; -import { loadSessionStore, resolveStorePath, updateSessionStore } from "../../config/sessions.js"; +import { + loadSessionStore, + resolveSessionStoreTargets, + resolveStorePath, + updateSessionStore, +} from "../../config/sessions.js"; import { mergeSessionEntry, type SessionAcpMeta, @@ -90,12 +92,11 @@ export async function listAcpSessionEntries(params: { cfg?: OpenClawConfig; }): Promise { const cfg = params.cfg ?? loadConfig(); - const stateDir = resolveStateDir(process.env); - const sessionDirs = await resolveAgentSessionDirs(stateDir); + const storeTargets = resolveSessionStoreTargets(cfg, { allAgents: true }); const entries: AcpSessionStoreEntry[] = []; - for (const sessionsDir of sessionDirs) { - const storePath = path.join(sessionsDir, "sessions.json"); + for (const target of storeTargets) { + const storePath = target.storePath; let store: Record; try { store = loadSessionStore(storePath); diff --git a/src/commands/session-store-targets.test.ts b/src/commands/session-store-targets.test.ts index 62ccab8d3cd..04c65db898d 100644 --- a/src/commands/session-store-targets.test.ts +++ b/src/commands/session-store-targets.test.ts @@ -1,11 +1,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { resolveSessionStoreTargets } from "./session-store-targets.js"; +import { resolveSessionStoreTargets } from "../config/sessions/targets.js"; const resolveStorePathMock = vi.hoisted(() => vi.fn()); const resolveDefaultAgentIdMock = vi.hoisted(() => vi.fn()); const listAgentIdsMock = vi.hoisted(() => vi.fn()); -vi.mock("../config/sessions.js", () => ({ +vi.mock("../config/sessions/paths.js", () => ({ resolveStorePath: resolveStorePathMock, })); diff --git a/src/commands/session-store-targets.ts b/src/commands/session-store-targets.ts index c9e91006e53..8e71cecc797 100644 --- a/src/commands/session-store-targets.ts +++ b/src/commands/session-store-targets.ts @@ -1,84 +1,9 @@ -import { listAgentIds, resolveDefaultAgentId } from "../agents/agent-scope.js"; -import { resolveStorePath } from "../config/sessions.js"; +export { resolveSessionStoreTargets } from "../config/sessions.js"; +import { resolveSessionStoreTargets } from "../config/sessions.js"; +import type { SessionStoreSelectionOptions, SessionStoreTarget } from "../config/sessions.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { normalizeAgentId } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; - -export type SessionStoreSelectionOptions = { - store?: string; - agent?: string; - allAgents?: boolean; -}; - -export type SessionStoreTarget = { - agentId: string; - storePath: string; -}; - -function dedupeTargetsByStorePath(targets: SessionStoreTarget[]): SessionStoreTarget[] { - const deduped = new Map(); - for (const target of targets) { - if (!deduped.has(target.storePath)) { - deduped.set(target.storePath, target); - } - } - return [...deduped.values()]; -} - -export function resolveSessionStoreTargets( - cfg: OpenClawConfig, - opts: SessionStoreSelectionOptions, -): SessionStoreTarget[] { - const defaultAgentId = resolveDefaultAgentId(cfg); - const hasAgent = Boolean(opts.agent?.trim()); - const allAgents = opts.allAgents === true; - if (hasAgent && allAgents) { - throw new Error("--agent and --all-agents cannot be used together"); - } - if (opts.store && (hasAgent || allAgents)) { - throw new Error("--store cannot be combined with --agent or --all-agents"); - } - - if (opts.store) { - return [ - { - agentId: defaultAgentId, - storePath: resolveStorePath(opts.store, { agentId: defaultAgentId }), - }, - ]; - } - - if (allAgents) { - const targets = listAgentIds(cfg).map((agentId) => ({ - agentId, - storePath: resolveStorePath(cfg.session?.store, { agentId }), - })); - return dedupeTargetsByStorePath(targets); - } - - if (hasAgent) { - const knownAgents = listAgentIds(cfg); - const requested = normalizeAgentId(opts.agent ?? ""); - if (!knownAgents.includes(requested)) { - throw new Error( - `Unknown agent id "${opts.agent}". Use "openclaw agents list" to see configured agents.`, - ); - } - return [ - { - agentId: requested, - storePath: resolveStorePath(cfg.session?.store, { agentId: requested }), - }, - ]; - } - - return [ - { - agentId: defaultAgentId, - storePath: resolveStorePath(cfg.session?.store, { agentId: defaultAgentId }), - }, - ]; -} +export type { SessionStoreSelectionOptions, SessionStoreTarget } from "../config/sessions.js"; export function resolveSessionStoreTargetsOrExit(params: { cfg: OpenClawConfig; diff --git a/src/config/sessions.ts b/src/config/sessions.ts index 701870ec8a7..1a521836405 100644 --- a/src/config/sessions.ts +++ b/src/config/sessions.ts @@ -11,3 +11,4 @@ export * from "./sessions/transcript.js"; export * from "./sessions/session-file.js"; export * from "./sessions/delivery-info.js"; export * from "./sessions/disk-budget.js"; +export * from "./sessions/targets.js"; diff --git a/src/config/sessions/targets.ts b/src/config/sessions/targets.ts new file mode 100644 index 00000000000..d00c2016503 --- /dev/null +++ b/src/config/sessions/targets.ts @@ -0,0 +1,80 @@ +import { listAgentIds, resolveDefaultAgentId } from "../../agents/agent-scope.js"; +import { normalizeAgentId } from "../../routing/session-key.js"; +import type { OpenClawConfig } from "../types.openclaw.js"; +import { resolveStorePath } from "./paths.js"; + +export type SessionStoreSelectionOptions = { + store?: string; + agent?: string; + allAgents?: boolean; +}; + +export type SessionStoreTarget = { + agentId: string; + storePath: string; +}; + +function dedupeTargetsByStorePath(targets: SessionStoreTarget[]): SessionStoreTarget[] { + const deduped = new Map(); + for (const target of targets) { + if (!deduped.has(target.storePath)) { + deduped.set(target.storePath, target); + } + } + return [...deduped.values()]; +} + +export function resolveSessionStoreTargets( + cfg: OpenClawConfig, + opts: SessionStoreSelectionOptions, +): SessionStoreTarget[] { + const defaultAgentId = resolveDefaultAgentId(cfg); + const hasAgent = Boolean(opts.agent?.trim()); + const allAgents = opts.allAgents === true; + if (hasAgent && allAgents) { + throw new Error("--agent and --all-agents cannot be used together"); + } + if (opts.store && (hasAgent || allAgents)) { + throw new Error("--store cannot be combined with --agent or --all-agents"); + } + + if (opts.store) { + return [ + { + agentId: defaultAgentId, + storePath: resolveStorePath(opts.store, { agentId: defaultAgentId }), + }, + ]; + } + + if (allAgents) { + const targets = listAgentIds(cfg).map((agentId) => ({ + agentId, + storePath: resolveStorePath(cfg.session?.store, { agentId }), + })); + return dedupeTargetsByStorePath(targets); + } + + if (hasAgent) { + const knownAgents = listAgentIds(cfg); + const requested = normalizeAgentId(opts.agent ?? ""); + if (!knownAgents.includes(requested)) { + throw new Error( + `Unknown agent id "${opts.agent}". Use "openclaw agents list" to see configured agents.`, + ); + } + return [ + { + agentId: requested, + storePath: resolveStorePath(cfg.session?.store, { agentId: requested }), + }, + ]; + } + + return [ + { + agentId: defaultAgentId, + storePath: resolveStorePath(cfg.session?.store, { agentId: defaultAgentId }), + }, + ]; +}