diff --git a/CHANGELOG.md b/CHANGELOG.md index b4222fb3337..6f445db8d10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai - Dreaming/diary: use the host local timezone for diary timestamps when `dreaming.timezone` is unset, so `DREAMS.md` and the UI stop defaulting to UTC. (#65034) Thanks @neo1027144-creator and @vincentkoc. - Plugins/memory: restore cached memory capability public artifacts on plugin-registry cache hits so memory-backed artifact surfaces stay visible after warm loads. Thanks @sercada and @vincentkoc. - Gateway/cron: preserve requested isolated-agent config across runtime reloads so subagent jobs and heartbeat overrides keep the right workspace and heartbeat settings when the hot-loaded snapshot is stale. Thanks @l0cka and @vincentkoc. +- Gateway/plugins: always send a non-empty `idempotencyKey` for plugin subagent runs, so dreaming narrative jobs stop failing gateway schema validation. (#65354) Thanks @CodeForgeNet and @vincentkoc. - Cron/isolated sessions: persist the right transcript path for each isolated run, including fresh session rollovers, so cron runs stop appending to stale session files. Thanks @samrusani and @vincentkoc. - Dreaming/cron: wake managed dreaming jobs immediately instead of waiting for the next heartbeat, so scheduled dreaming runs start when the cron fires. (#65053) Thanks @l0cka and @vincentkoc. diff --git a/src/gateway/server-plugins.test.ts b/src/gateway/server-plugins.test.ts index 59a0b2a00be..1d047880181 100644 --- a/src/gateway/server-plugins.test.ts +++ b/src/gateway/server-plugins.test.ts @@ -556,6 +556,46 @@ describe("loadGatewayPlugins", () => { }); }); + test("forwards caller-supplied idempotencyKey on subagent run", async () => { + const serverPlugins = serverPluginsModule; + const runtime = await createSubagentRuntime(serverPlugins); + serverPlugins.setFallbackGatewayContext(createTestContext("idempotency-forward")); + + await runtime.run({ + sessionKey: "s-idem-forward", + message: "hello", + deliver: false, + idempotencyKey: "caller-provided-key", + }); + + expect(getLastDispatchedParams()).toMatchObject({ + sessionKey: "s-idem-forward", + message: "hello", + idempotencyKey: "caller-provided-key", + }); + }); + + test("generates a non-empty idempotencyKey when the caller omits it", async () => { + const serverPlugins = serverPluginsModule; + const runtime = await createSubagentRuntime(serverPlugins); + serverPlugins.setFallbackGatewayContext(createTestContext("idempotency-generate")); + + await runtime.run({ + sessionKey: "s-idem-generate", + message: "hello", + deliver: false, + }); + + const params = getLastDispatchedParams(); + expect(params).toBeDefined(); + // The gateway `agent` schema requires `idempotencyKey: NonEmptyString`, so + // the runtime must always send a populated value. A missing field here + // would reproduce the memory-core dreaming-narrative regression. + const generated = params?.idempotencyKey; + expect(typeof generated).toBe("string"); + expect((generated as string).length).toBeGreaterThan(0); + }); + test("rejects provider/model overrides for fallback runs without explicit authorization", async () => { const serverPlugins = serverPluginsModule; const runtime = await createSubagentRuntime(serverPlugins); diff --git a/src/gateway/server-plugins.ts b/src/gateway/server-plugins.ts index 7b561f1048f..71b5d4d6af0 100644 --- a/src/gateway/server-plugins.ts +++ b/src/gateway/server-plugins.ts @@ -338,7 +338,11 @@ export function createGatewaySubagentRuntime(): PluginRuntime["subagent"] { ...(allowOverride && params.model && { model: params.model }), ...(params.extraSystemPrompt && { extraSystemPrompt: params.extraSystemPrompt }), ...(params.lane && { lane: params.lane }), - ...(params.idempotencyKey && { idempotencyKey: params.idempotencyKey }), + // The gateway `agent` schema requires `idempotencyKey: NonEmptyString`, + // so fall back to a generated UUID when the caller omits it. Without + // this, plugin subagent runs (for example memory-core dreaming + // narrative) silently fail schema validation at the gateway. + idempotencyKey: params.idempotencyKey || randomUUID(), }, { allowSyntheticModelOverride,