diff --git a/extensions/slack/src/monitor/message-handler.test.ts b/extensions/slack/src/monitor/message-handler.test.ts index ea58d560a4f1..e4a3d84e3ee4 100644 --- a/extensions/slack/src/monitor/message-handler.test.ts +++ b/extensions/slack/src/monitor/message-handler.test.ts @@ -19,6 +19,7 @@ const onFlushCallbacks: Array< > = []; const prepareSlackMessageMock = vi.fn( async (_params?: { + ctx: Parameters[0]["ctx"]; opts: { onVisibleDrop?: () => void }; }): Promise<{ ctxPayload: Record } | null> => ({ ctxPayload: {} }), ); @@ -174,6 +175,59 @@ describe("createSlackMessageHandler", () => { expect(context.cfg).toBe(startupConfig); }); + it("keeps cached runtime contexts synchronized with mutable monitor state", async () => { + const startupConfig: OpenClawConfig = { agents: { defaults: { thinkingDefault: "max" } } }; + const runtimeConfig: OpenClawConfig = { agents: { defaults: { thinkingDefault: "ultra" } } }; + const initialChannels = { C_OLD: { enabled: true } }; + const resolvedChannels = { C_RESOLVED: { enabled: true } }; + const context = createContext({ cfg: startupConfig }); + context.botUserId = "U_STALE"; + context.channelsConfig = initialChannels; + const handler = createSlackMessageHandler({ + ctx: context, + account: { accountId: "default" } as Parameters< + typeof createSlackMessageHandler + >[0]["account"], + }); + setRuntimeConfigSnapshot(runtimeConfig, runtimeConfig); + + const handleMessage = async (ts: string) => { + await handler( + { + type: "message", + channel: "D1", + user: "U1", + ts, + text: "hello", + } as never, + { source: "message" }, + ); + const entry = enqueueMock.mock.calls.at(-1)?.[0] as Record; + await runOnFlush([entry]); + }; + + await handleMessage("1709000000.009007"); + const initialRuntimeContext = prepareSlackMessageMock.mock.calls[0]?.[0]?.ctx; + expect(initialRuntimeContext).toMatchObject({ + cfg: runtimeConfig, + botUserId: "U_STALE", + channelsConfig: initialChannels, + }); + + context.botUserId = "U_RECOVERED"; + context.channelsConfig = resolvedChannels; + await handleMessage("1709000000.009008"); + + const reusedRuntimeContext = prepareSlackMessageMock.mock.calls[1]?.[0]?.ctx; + expect(reusedRuntimeContext).toBe(initialRuntimeContext); + expect(reusedRuntimeContext).toMatchObject({ + cfg: runtimeConfig, + botUserId: "U_RECOVERED", + channelsConfig: resolvedChannels, + }); + expect(context.cfg).toBe(startupConfig); + }); + it.each([ { label: "without a source snapshot", diff --git a/extensions/slack/src/monitor/message-handler.ts b/extensions/slack/src/monitor/message-handler.ts index 555a8c58c908..24622d8425fb 100644 --- a/extensions/slack/src/monitor/message-handler.ts +++ b/extensions/slack/src/monitor/message-handler.ts @@ -132,7 +132,9 @@ export function createSlackMessageHandler(params: { if (cached) { return cached; } - const runtimeContext = { ...ctx, cfg: runtimeConfig }; + // Keep identity, allowlists, and other mutable monitor state live while pinning this config. + const runtimeContext = Object.create(ctx) as SlackMonitorContext; + runtimeContext.cfg = runtimeConfig; runtimeContexts.set(runtimeConfig, runtimeContext); return runtimeContext; };