diff --git a/CHANGELOG.md b/CHANGELOG.md index ac14e4cee53..0c813ada47f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai - Docs/security hardening guidance: document Docker `DOCKER-USER` + UFW policy and add cross-linking from Docker install docs for VPS/public-host setups. (#27613) thanks @dorukardahan. - Docs/tool-loop detection config keys: align `docs/tools/loop-detection.md` examples and field names with the current `tools.loopDetection` schema to prevent copy-paste validation failures from outdated keys. (#33182) Thanks @Mylszd. +- Gateway/session agent discovery: include disk-scanned agent IDs in `listConfiguredAgentIds` even when `agents.list` is configured, so disk-only/ACP agent sessions remain visible in gateway session aggregation and listings. (#32831) thanks @Sid-Qin. - Discord/inbound debouncer: skip bot-own MESSAGE_CREATE events before they reach the debounce queue to avoid self-triggered slowdowns in busy servers. Thanks @thewilloftheshadow. - Discord/Agent-scoped media roots: pass `mediaLocalRoots` through Discord monitor reply delivery (message + component interaction paths) so local media attachments honor per-agent workspace roots instead of falling back to default global roots. Thanks @thewilloftheshadow. - Discord/slash command handling: intercept text-based slash commands in channels, register plugin commands as native, and send fallback acknowledgments for empty slash runs so interactions do not hang. Thanks @thewilloftheshadow. diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index ff090f2248f..943aea46e90 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -4,12 +4,14 @@ import path from "node:path"; import { describe, expect, test } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import type { SessionEntry } from "../config/sessions.js"; +import { withStateDirEnv } from "../test-helpers/state-dir-env.js"; import { capArrayByJsonBytes, classifySessionKey, deriveSessionTitle, listAgentsForGateway, listSessionsFromStore, + loadCombinedSessionStoreForGateway, parseGroupKey, pruneLegacyStoreKeys, resolveGatewaySessionStoreTarget, @@ -310,6 +312,21 @@ describe("gateway session utils", () => { `data:image/png;base64,${Buffer.from("avatar").toString("base64")}`, ); }); + + test("listAgentsForGateway keeps explicit agents.list scope over disk-only agents (scope boundary)", async () => { + await withStateDirEnv("openclaw-agent-list-scope-", async ({ stateDir }) => { + fs.mkdirSync(path.join(stateDir, "agents", "main"), { recursive: true }); + fs.mkdirSync(path.join(stateDir, "agents", "codex"), { recursive: true }); + + const cfg = { + session: { mainKey: "main" }, + agents: { list: [{ id: "main", default: true }] }, + } as OpenClawConfig; + + const { agents } = listAgentsForGateway(cfg); + expect(agents.map((agent) => agent.id)).toEqual(["main"]); + }); + }); }); describe("resolveSessionModelRef", () => { @@ -746,3 +763,45 @@ describe("listSessionsFromStore search", () => { expect(missing?.totalTokensFresh).toBe(false); }); }); + +describe("loadCombinedSessionStoreForGateway includes disk-only agents (#32804)", () => { + test("ACP agent sessions are visible even when agents.list is configured", async () => { + await withStateDirEnv("openclaw-acp-vis-", async ({ stateDir }) => { + const agentsDir = path.join(stateDir, "agents"); + const mainDir = path.join(agentsDir, "main", "sessions"); + const codexDir = path.join(agentsDir, "codex", "sessions"); + fs.mkdirSync(mainDir, { recursive: true }); + fs.mkdirSync(codexDir, { recursive: true }); + + fs.writeFileSync( + path.join(mainDir, "sessions.json"), + JSON.stringify({ + "agent:main:main": { sessionId: "s-main", updatedAt: 100 }, + }), + "utf8", + ); + + fs.writeFileSync( + path.join(codexDir, "sessions.json"), + JSON.stringify({ + "agent:codex:acp-task": { sessionId: "s-codex", updatedAt: 200 }, + }), + "utf8", + ); + + const cfg = { + session: { + mainKey: "main", + store: path.join(stateDir, "agents", "{agentId}", "sessions", "sessions.json"), + }, + agents: { + list: [{ id: "main", default: true }], + }, + } as OpenClawConfig; + + const { store } = loadCombinedSessionStoreForGateway(cfg); + expect(store["agent:main:main"]).toBeDefined(); + expect(store["agent:codex:acp-task"]).toBeDefined(); + }); + }); +}); diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index fa4c514388b..969c60c378c 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -310,35 +310,25 @@ function listExistingAgentIdsFromDisk(): string[] { } function listConfiguredAgentIds(cfg: OpenClawConfig): string[] { - const agents = cfg.agents?.list ?? []; - if (agents.length > 0) { - const ids = new Set(); - for (const entry of agents) { - if (entry?.id) { - ids.add(normalizeAgentId(entry.id)); - } - } - const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg)); - ids.add(defaultId); - const sorted = Array.from(ids).filter(Boolean); - sorted.sort((a, b) => a.localeCompare(b)); - return sorted.includes(defaultId) - ? [defaultId, ...sorted.filter((id) => id !== defaultId)] - : sorted; - } - const ids = new Set(); const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg)); ids.add(defaultId); + + for (const entry of cfg.agents?.list ?? []) { + if (entry?.id) { + ids.add(normalizeAgentId(entry.id)); + } + } + for (const id of listExistingAgentIdsFromDisk()) { ids.add(id); } + const sorted = Array.from(ids).filter(Boolean); sorted.sort((a, b) => a.localeCompare(b)); - if (sorted.includes(defaultId)) { - return [defaultId, ...sorted.filter((id) => id !== defaultId)]; - } - return sorted; + return sorted.includes(defaultId) + ? [defaultId, ...sorted.filter((id) => id !== defaultId)] + : sorted; } export function listAgentsForGateway(cfg: OpenClawConfig): {