From 65ae1e54de3182c5b32b31649e91afc6370d0982 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 22 Apr 2026 14:18:35 -0700 Subject: [PATCH] fix(hooks): avoid stale thread ownership startup fallback --- extensions/thread-ownership/index.test.ts | 26 +++++++++++++++++++ extensions/thread-ownership/index.ts | 7 +++-- .../contracts/boundary-invariants.test.ts | 3 ++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/extensions/thread-ownership/index.test.ts b/extensions/thread-ownership/index.test.ts index e21b8758111..5915827aff7 100644 --- a/extensions/thread-ownership/index.test.ts +++ b/extensions/thread-ownership/index.test.ts @@ -211,6 +211,32 @@ describe("thread-ownership plugin", () => { expect(globalThis.fetch).not.toHaveBeenCalled(); }); + it("does not fall back to startup allowlists when live plugin config is removed", async () => { + api.pluginConfig = { abTestChannels: ["C999"] }; + register.register(api as unknown as OpenClawPluginApi); + vi.mocked(globalThis.fetch).mockResolvedValue( + new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }), + ); + + const result = await hooks.message_sending( + { + content: "hello", + replyToId: "1234.5678", + to: "C123", + }, + { channelId: "slack", conversationId: "C123" }, + ); + + expect(result).toBeUndefined(); + expect(globalThis.fetch).toHaveBeenCalledWith( + "http://localhost:8750/api/v1/ownership/C123/1234.5678", + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ agent_id: "test-agent" }), + }), + ); + }); + it("cancels when thread owned by another agent", async () => { vi.mocked(globalThis.fetch).mockResolvedValue( new Response(JSON.stringify({ owner: "other-agent" }), { status: 409 }), diff --git a/extensions/thread-ownership/index.ts b/extensions/thread-ownership/index.ts index 0d1357c9a75..d532127cdf9 100644 --- a/extensions/thread-ownership/index.ts +++ b/extensions/thread-ownership/index.ts @@ -83,11 +83,14 @@ export default definePluginEntry({ description: "Slack thread claim coordination for multi-agent setups", register(api: OpenClawPluginApi) { const resolveCurrentState = () => { - const currentConfig = api.runtime.config?.loadConfig?.() ?? api.config; + const runtimeConfigAvailable = typeof api.runtime.config?.loadConfig === "function"; + const currentConfig = runtimeConfigAvailable + ? (api.runtime.config.loadConfig() ?? api.config) + : api.config; const livePluginCfg = resolvePluginConfigObject(currentConfig, "thread-ownership"); const pluginCfg = isThreadOwnershipConfig(livePluginCfg) ? livePluginCfg - : isThreadOwnershipConfig(api.pluginConfig) + : !runtimeConfigAvailable && isThreadOwnershipConfig(api.pluginConfig) ? api.pluginConfig : {}; return { diff --git a/src/plugins/contracts/boundary-invariants.test.ts b/src/plugins/contracts/boundary-invariants.test.ts index 44b71bd515d..ee6fbfec01a 100644 --- a/src/plugins/contracts/boundary-invariants.test.ts +++ b/src/plugins/contracts/boundary-invariants.test.ts @@ -70,7 +70,8 @@ const BUNDLED_LIVE_CONFIG_HOOK_GUARDS = { ], "extensions/thread-ownership/index.ts": [ 'resolvePluginConfigObject(currentConfig, "thread-ownership")', - "api.runtime.config?.loadConfig?.() ?? api.config", + 'typeof api.runtime.config?.loadConfig === "function"', + "api.runtime.config.loadConfig() ?? api.config", ], } as const satisfies Record;