From c8aed9b1ef671731c285ca0691d97cf2d019698f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 16 Aug 2026 21:58:33 -0700 Subject: [PATCH] fix(gateway): source agent provenance from roster owner (#125032) --- src/gateway/server-methods/agents.ts | 23 +++--------------- .../server.chat.gateway-server-chat-b.test.ts | 24 ++++++++++++++++--- src/gateway/session-utils-store.ts | 14 ++++++++++- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/gateway/server-methods/agents.ts b/src/gateway/server-methods/agents.ts index 8034d24e5d74..9b80e4e58c06 100644 --- a/src/gateway/server-methods/agents.ts +++ b/src/gateway/server-methods/agents.ts @@ -93,7 +93,6 @@ import { readAgentDeletionJournal, type AgentDeletionJournalCleanupPath, } from "../../state/agent-deletion-journal.js"; -import { listAgentProvenance } from "../../state/agent-provenance.js"; import { assertNoOpenClawAgentDatabaseLeases } from "../../state/openclaw-agent-db-lease.js"; import { unregisterOpenClawAgentDatabase } from "../../state/openclaw-agent-db-registry.js"; import { @@ -899,27 +898,11 @@ export const agentsHandlers: GatewayRequestHandlers = { const cfg = context.getRuntimeConfig(); const modelCatalog = await readPreparedServerMethodModelCatalog(context); - const result = listAgentsForGateway(cfg, modelCatalog, { - includeSystem: hasGatewayClientCap(client?.connect.caps, GATEWAY_CLIENT_CAPS.AGENT_KIND), - }); - const provenanceById = new Map( - listAgentProvenance().map((record) => [record.agentId, record] as const), - ); respond( true, - { - ...result, - agents: result.agents.map((agent) => { - const provenance = provenanceById.get(agent.id); - return provenance - ? Object.assign({}, agent, { - createdVia: provenance.createdVia, - creatorAgentId: provenance.creatorAgentId, - createdAt: provenance.createdAtMs, - }) - : agent; - }), - }, + listAgentsForGateway(cfg, modelCatalog, { + includeSystem: hasGatewayClientCap(client?.connect.caps, GATEWAY_CLIENT_CAPS.AGENT_KIND), + }), undefined, ); }, diff --git a/src/gateway/server.chat.gateway-server-chat-b.test.ts b/src/gateway/server.chat.gateway-server-chat-b.test.ts index 31aca3d88fca..f6c2168e05f0 100644 --- a/src/gateway/server.chat.gateway-server-chat-b.test.ts +++ b/src/gateway/server.chat.gateway-server-chat-b.test.ts @@ -40,6 +40,7 @@ import { runExclusiveSessionLifecycleMutation, } from "../sessions/session-lifecycle-admission.js"; import { buildPersistedUserTurnMessage } from "../sessions/user-turn-transcript.js"; +import { recordAgentProvenance } from "../state/agent-provenance.js"; import { openOpenClawAgentDatabase } from "../state/openclaw-agent-db.js"; import { captureEnv, setTestEnvValue } from "../test-utils/env.js"; import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js"; @@ -1123,7 +1124,7 @@ describe("gateway server chat", () => { }); }); - test("chat.startup returns chat history with the initial agents list", async () => { + test("chat.startup returns chat history with the initial agents list and provenance", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { await writeGatewayConfig({ agents: { @@ -1135,7 +1136,7 @@ describe("gateway server chat", () => { "openai/gpt-main": {}, }, }, - entries: { main: { default: true } }, + entries: { main: { default: true }, research: {} }, }, models: { providers: { @@ -1146,6 +1147,11 @@ describe("gateway server chat", () => { }, }, }); + recordAgentProvenance( + "research", + { createdVia: "agent", creatorAgentId: "main" }, + { nowMs: 42 }, + ); await connectOk(ws); await createSessionDir(); const updatedAt = Date.now(); @@ -1160,7 +1166,12 @@ describe("gateway server chat", () => { const startup = await rpcReq<{ agentsList?: { - agents?: Array<{ id?: string }>; + agents?: Array<{ + id?: string; + createdVia?: string; + creatorAgentId?: string | null; + createdAt?: number; + }>; defaultId?: string | null; mainKey?: string | null; }; @@ -1176,6 +1187,13 @@ describe("gateway server chat", () => { expect(startup.payload?.agentsList?.defaultId).toBe("main"); expect(startup.payload?.agentsList?.mainKey).toBe("main"); expect(startup.payload?.agentsList?.agents?.map((agent) => agent.id)).toContain("main"); + expect( + startup.payload?.agentsList?.agents?.find((agent) => agent.id === "research"), + ).toMatchObject({ + createdVia: "agent", + creatorAgentId: "main", + createdAt: 42, + }); expect(startup.payload?.sessionInfo).toMatchObject({ key: "agent:main:main", sessionId: "sess-main", diff --git a/src/gateway/session-utils-store.ts b/src/gateway/session-utils-store.ts index d4a4d133f224..5d10790ed40e 100644 --- a/src/gateway/session-utils-store.ts +++ b/src/gateway/session-utils-store.ts @@ -30,6 +30,7 @@ import { canonicalSessionKeyMigrationRequiredError } from "../config/sessions/se import type { OpenClawConfig } from "../config/types.openclaw.js"; import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js"; import { isAcpSessionKey } from "../sessions/session-key-utils.js"; +import { listAgentProvenance } from "../state/agent-provenance.js"; import { listGatewayAgentsBasic } from "./agent-list.js"; import type { GatewayAgentOwnership } from "./agent-list.js"; import { tryResolveSessionCompatibilityOwnerAgentId } from "./session-request-agent.js"; @@ -337,6 +338,9 @@ export function listAgentsForGateway( const roster = options?.includeSystem ? basic.agents : basic.agents.filter((entry) => entry.kind !== "system"); + const provenanceById = new Map( + listAgentProvenance().map((record) => [record.agentId, record] as const), + ); const agents = roster.map((entry) => { const { id } = entry; const meta = configuredById.get(id); @@ -366,7 +370,7 @@ export function listAgentsForGateway( // Must mirror the sessions.create worktree preflight: subdirectory workspaces inside a // repo are worktree-capable, so the UI toggle and the create path cannot diverge. const workspaceGit = insideGitCheckout(workspace); - return Object.assign( + const agent = Object.assign( { id, ...(options?.includeSystem ? { kind: entry.kind } : {}), @@ -382,6 +386,14 @@ export function listAgentsForGateway( }, { model }, ); + const provenance = provenanceById.get(id); + return provenance + ? Object.assign(agent, { + createdVia: provenance.createdVia, + creatorAgentId: provenance.creatorAgentId, + createdAt: provenance.createdAtMs, + }) + : agent; }); return { defaultId: basic.defaultId,