From 35576d14812c21e4b2533435803a6c208f6d648f Mon Sep 17 00:00:00 2001 From: spiceoogway Date: Fri, 30 Jan 2026 08:51:46 -0500 Subject: [PATCH 1/2] fix(nostr): replace non-existent handleInboundMessage API with dispatchReplyFromConfig Fixes #4547 The Nostr plugin was calling runtime.channel.reply.handleInboundMessage(), which doesn't exist in the PluginRuntime API. This caused DM processing to fail with 'handleInboundMessage is not a function'. Changed to use the correct message dispatch pattern: 1. Build proper MsgContext with finalizeInboundContext() 2. Create reply dispatcher with createReplyDispatcherWithTyping() 3. Dispatch replies with dispatchReplyFromConfig() This matches the pattern used by other channel extensions like Matrix and Discord. The fix ensures: - DMs are properly routed through the agent pipeline - Session state is recorded correctly - Reply text is formatted with markdown table conversion - Human delay and typing indicators work as expected --- extensions/nostr/src/channel.ts | 101 +++++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 7 deletions(-) diff --git a/extensions/nostr/src/channel.ts b/extensions/nostr/src/channel.ts index 990c06f88..a3cdd6ad1 100644 --- a/extensions/nostr/src/channel.ts +++ b/extensions/nostr/src/channel.ts @@ -221,18 +221,105 @@ export const nostrPlugin: ChannelPlugin = { onMessage: async (senderPubkey, text, reply) => { ctx.log?.debug(`[${account.accountId}] DM from ${senderPubkey}: ${text.slice(0, 50)}...`); - // Forward to OpenClaw's message pipeline - await runtime.channel.reply.handleInboundMessage({ + // Load current config for routing + const cfg = runtime.config.loadConfig(); + + // Resolve agent route for this message + const route = runtime.channel.routing.resolveAgentRoute({ + cfg, channel: "nostr", accountId: account.accountId, - senderId: senderPubkey, chatType: "direct", - chatId: senderPubkey, // For DMs, chatId is the sender's pubkey - text, - reply: async (responseText: string) => { - await reply(responseText); + senderId: senderPubkey, + }); + + // Build session key for this conversation + const sessionKey = `nostr:${account.accountId}:direct:${senderPubkey}`; + + // Format the nostr prefix for sender + const from = `nostr:${senderPubkey}`; + const to = `nostr:${account.publicKey}`; + + // Build the message context + const msgContext = runtime.channel.reply.finalizeInboundContext({ + Body: text, + RawBody: text, + CommandBody: text, + From: from, + To: to, + SessionKey: sessionKey, + AccountId: account.accountId, + ChatType: "direct" as const, + ConversationLabel: `Nostr DM`, + SenderName: senderPubkey.slice(0, 8), + SenderId: senderPubkey, + Provider: "nostr" as const, + Surface: "nostr" as const, + Timestamp: Date.now(), + CommandAuthorized: true, + CommandSource: "text" as const, + OriginatingChannel: "nostr" as const, + OriginatingTo: to, + }); + + // Record the inbound session + const storePath = runtime.channel.session.resolveStorePath(cfg.session?.store, { + agentId: route.agentId, + }); + + await runtime.channel.session.recordInboundSession({ + storePath, + sessionKey, + ctx: msgContext, + updateLastRoute: { + sessionKey: route.mainSessionKey, + channel: "nostr", + to: from, + accountId: account.accountId, + }, + onRecordError: (err) => { + ctx.log?.debug(`[${account.accountId}] Failed updating session meta: ${String(err)}`); }, }); + + // Create reply dispatcher with typing + const tableMode = runtime.channel.text.resolveMarkdownTableMode({ + cfg, + channel: "nostr", + accountId: account.accountId, + }); + + const { dispatcher, replyOptions, markDispatchIdle } = + runtime.channel.reply.createReplyDispatcherWithTyping({ + responsePrefix: null, + responsePrefixContextProvider: null, + humanDelay: runtime.channel.reply.resolveHumanDelayConfig(cfg, route.agentId), + deliver: async (payload) => { + const replyText = payload.text ?? ""; + const convertedText = runtime.channel.text.convertMarkdownTables(replyText, tableMode); + await reply(convertedText); + }, + onError: (err, info) => { + ctx.log?.error(`[${account.accountId}] Nostr ${info.kind} reply failed: ${String(err)}`); + }, + }); + + // Dispatch the reply + const { queuedFinal, counts } = await runtime.channel.reply.dispatchReplyFromConfig({ + ctx: msgContext, + cfg, + dispatcher, + replyOptions, + }); + + markDispatchIdle(); + + if (queuedFinal) { + const finalCount = counts.final; + ctx.log?.debug( + `[${account.accountId}] Delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${senderPubkey.slice(0, 8)}`, + ); + } }, onError: (error, context) => { ctx.log?.error(`[${account.accountId}] Nostr error (${context}): ${error.message}`); From 38d2e1ebba3536c5ed84a9afdac54fde4c70cdfc Mon Sep 17 00:00:00 2001 From: spiceoogway Date: Fri, 30 Jan 2026 09:06:06 -0500 Subject: [PATCH 2/2] fix: use agent-specific model config in allowlist validation Fixes #4587 When an agent has a specific model configured via agents.list[].model, the model was correctly resolved but then incorrectly validated against the global model allowlist instead of the agent-specific config. This caused the agent to fall back to the default model even when a specific model was configured for that agent. The fix ensures that buildAllowedModelSet uses the same modified config (cfgForModelSelection) that includes the agent-specific model override, rather than the original config. Example config that was broken: { "agents": { "list": [ { "id": "researcher", "model": "anthropic/claude-opus-4-5" } ], "defaults": { "models": { "anthropic/claude-sonnet-4-5": { "alias": "sonnet" } } } } } Before: researcher agent would use sonnet (allowlist rejected opus) After: researcher agent uses opus (allowlist includes agent-specific model) --- src/commands/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 0bcb444b9..38fcfd92b 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -265,7 +265,7 @@ export async function agentCommand( if (needsModelCatalog) { modelCatalog = await loadModelCatalog({ config: cfg }); const allowed = buildAllowedModelSet({ - cfg, + cfg: cfgForModelSelection, catalog: modelCatalog, defaultProvider, defaultModel,