feat: core primitives for significant extension capability improvements#186
feat: core primitives for significant extension capability improvements#186dgarson wants to merge 714 commits intodgarson/forkfrom
Conversation
When sessions_spawn is called without runTimeoutSeconds, subagents previously defaulted to 0 (no timeout). This adds a config key at agents.defaults.subagents.runTimeoutSeconds so operators can set a global default timeout for all subagent runs. The agent-provided value still takes precedence when explicitly passed. When neither the agent nor the config specifies a timeout, behavior is unchanged (0 = no timeout), preserving backwards compatibility. Updated for the subagent-spawn.ts refactor (logic moved from sessions-spawn-tool.ts to spawnSubagentDirect). Closes openclaw#19288 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Matches convention used by all other *Seconds/*Ms timeout fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit 816a6b3)
When addGatewayClientOptions registers --url on the parent browser command, Commander.js captures it before the cookies set subcommand can receive it. Switch from requiredOption to option and resolve via inheritOptionFromParent, matching the existing pattern used for --target-id. Fixes openclaw#24811 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> (cherry picked from commit 96fcb96)
The input peer.kind from channel plugins was used as-is without normalization via normalizeChatType(), while the binding side correctly normalized. This caused "dm" !== "direct" mismatches in matchesBindingScope, making plugins that use "dm" as peerKind fail to match bindings configured with "direct". Normalize both peer.kind and parentPeer.kind through normalizeChatType() so that "dm" and "direct" are treated equivalently on both sides. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> (cherry picked from commit b0c9670)
(cherry picked from commit 3c21fc3)
(cherry picked from commit 3b51540)
…enclaw#17266) (cherry picked from commit 644badd)
…ribe handler The handleAutoCompactionStart handler was calling runBeforeCompaction with only messageCount and an empty hook context. Plugins receiving this hook could not identify the session or snapshot the transcript during auto-compaction. The other call site in compact.ts already passes the full payload (messages, sessionFile, sessionKey). This aligns the subscribe handler to do the same using ctx.params.session and ctx.params.sessionKey. (cherry picked from commit 318a19d)
(cherry picked from commit 50fd31c)
…atch (cherry picked from commit d3bfbde)
…uncation repairToolUseResultPairing was gated behind !isOpenAi, skipping orphaned tool_result cleanup for OpenAI providers. When limitHistoryTurns truncated conversation history, tool_result messages whose matching tool_call was before the truncation point survived and were sent as function_call_output items with stale call_id references. OpenAI rejects these with: "No tool call found for function call output with call_id ..." Enable the repair universally — all providers need it after truncation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> (cherry picked from commit 97b065a)
…penAI The previous test asserted that OpenAI-responses sessions would NOT get synthetic tool results for orphaned tool calls. With repairToolUseResultPairing now running universally, the correct behavior is that orphaned tool calls get a synthetic tool_result — matching what OpenAI actually requires. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> (cherry picked from commit 2edb0ff)
…odel
The 'auto' model on OpenRouter dynamically routes to any underlying model
OpenRouter selects, including reasoning-required endpoints. Previously,
OpenClaw would unconditionally inject `reasoning.effort: "none"` into
every request when the thinking level was "off", which causes a 400 error
on models where reasoning is mandatory and cannot be disabled.
Root cause:
- openrouter/auto has reasoning: false in the built-in catalog
- With thinking level "off", createOpenRouterWrapper injects
`reasoning: { effort: "none" }` via mapThinkingLevelToOpenRouterReasoningEffort
- For any OpenRouter-routed model that requires reasoning this results in:
"400 Reasoning is mandatory for this endpoint and cannot be disabled"
- The reasoning: false is then persisted back to models.json on every
ensureOpenClawModelsJson call, so manually removing it has no lasting effect
Fix:
- In applyExtraParamsToAgent, when provider is "openrouter" and the model
id is "auto", pass undefined as thinkingLevel to createOpenRouterWrapper
so no reasoning.effort is injected at all, letting OpenRouter's upstream
model handle it natively
- Add an explanatory comment in buildOpenrouterProvider clarifying that the
reasoning: false catalog value does NOT cause effort injection for "auto"
Users who need explicit reasoning control should target a specific model
id (e.g. openrouter/deepseek/deepseek-r1) rather than the auto router.
Fixes openclaw#24851
(cherry picked from commit aa55439)
In trusted-proxy mode, sharedAuthResult is null because hasSharedAuth only triggers for token/password in connectParams.auth. But the primary auth (authResult) already validated the trusted-proxy — the connection came from a CIDR in trustedProxies with a valid userHeader. This IS shared auth semantically (the proxy vouches for identity), so operator connections should be able to skip device identity. Without this fix, trusted-proxy operator connections are rejected with "device identity required" because roleCanSkipDeviceIdentity() sees sharedAuthOk=false. (cherry picked from commit e87048a)
(cherry picked from commit bddeb1f)
…close (cherry picked from commit df827c3)
When reasoningLevel is 'on', reasoning content was being sent as a visible message to WhatsApp and other non-Telegram channels via two paths: 1. Block reply: emitted via onBlockReply in handleMessageEnd 2. Final payloads: added to replyItems in buildEmbeddedRunPayloads Telegram has its own dispatch path (bot-message-dispatch.ts) that splits reasoning into a dedicated lane and handles suppression. The generic dispatch-from-config.ts path used by WhatsApp, web, etc. had no such filtering. Fix: - Add isReasoning?: boolean flag to ReplyPayload - Tag reasoning payloads at both emission points - Filter isReasoning payloads in dispatch-from-config.ts for both block reply and final reply paths Telegram is unaffected: it uses its own deliver callback that detects reasoning via the 'Reasoning:\n' prefix and routes to a separate lane. Fixes openclaw#24954
When `includeReasoning` is active (or `reasoningLevel` falls back to the model default), the agent emits reasoning blocks as separate reply payloads prefixed with "Reasoning:\n". Matrix has no dedicated reasoning lane, so these internal thinking traces leak into the chat as regular user-visible messages. Filter out pure-reasoning payloads (those starting with "Reasoning:\n" or a `<thinking>` tag) before delivery so internal reasoning never reaches the Matrix room. Fixes openclaw#24411 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify that deliverMatrixReplies skips replies whose text starts with "Reasoning:\n" or opens with <thinking>/<think>/<antthinking> tags, while still delivering all normal replies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…#25435) Land PR openclaw#25435 from @Zjianru. Changelog: add 2026.2.24 fix entry with contributor credit. Co-authored-by: codez <codezhujr@gmail.com>
…penclaw#25682) Land PR openclaw#25682 from @lairtonlelis after maintainer rework: track dispatcher updates when network decision changes to avoid stale global fetch behavior. Co-authored-by: Ailton <lairton@telnyx.com>
…penclaw#25827) Carry fail-closed empty-allowlist guard clarity and changelog attribution for PR openclaw#25827. Co-authored-by: Brian Mendonca <brianmendonca@Brians-MacBook-Air.local>
) Land PR openclaw#25680 from @lairtonlelis. Retain explicit status/code/http 402 detection for oversized structured payloads. Co-authored-by: Ailton <lairton@telnyx.com>
…#25729) Land PR openclaw#25729 from @Suko. Use shared fallback-resolution helper and add regression coverage for default, override, and explicit-empty cases. Co-authored-by: suko <miha.sukic@gmail.com>
…ing hyphens Paths starting with "-" (like those containing "---" pattern) can be interpreted as shell options by the sh shell. This fix adds a helper function that prepends "./" to paths starting with "-" to prevent this interpretation. This fixes the issue where sandbox filesystem operations fail with "Syntax error: ; unexpected" when file paths contain the "---" pattern used in auto-generated inbound media filenames like: file_1095---f00a04a2-99a0-4d98-99b0-dfe61c5a4198.ogg 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
@mcaxtr) Co-authored-by: Marcus Castro <mcaxtr@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9ff544209b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| next: PluginHookBeforeAgentRunResult, | ||
| ): PluginHookBeforeAgentRunResult => ({ | ||
| // Once any hook rejects, keep that rejection (first reject wins). | ||
| reject: acc?.reject ?? next.reject, |
There was a problem hiding this comment.
Allow later before_agent_run hooks to reject
The merge logic uses acc?.reject ?? next.reject, so an earlier hook that returns { reject: false } prevents every later hook from ever rejecting. In multi-plugin setups, this lets a lower-priority policy gate (budget/rate-limit/HITL) be silently bypassed whenever a higher-priority hook returns a boolean reject field that happens to be false. The merge should treat rejection as sticky-true rather than first-defined-boolean.
Useful? React with 👍 / 👎.
| // First defined agentId/sessionKey wins; skip is sticky. | ||
| agentId: acc?.agentId ?? next.agentId, | ||
| sessionKey: acc?.sessionKey ?? next.sessionKey, | ||
| skip: acc?.skip ?? next.skip, |
There was a problem hiding this comment.
Let later before_message_route hooks set skip=true
Using acc?.skip ?? next.skip makes skip first-defined instead of sticky, so an earlier hook returning { skip: false } blocks any later hook from dropping the message. That breaks composability for routing/filter plugins where one hook may explicitly opt out while another must enforce a hard block.
Useful? React with 👍 / 👎.
| const agentPart = session.sessionId.split(":")[1]; | ||
| if (agentPart && !groupIds.has(agentPart)) { |
There was a problem hiding this comment.
Filter quota groupIds by real agent scope
The group filter assumes sessionId has colon-delimited agent parts, but discovered session IDs come from transcript filenames and typically do not contain :. In that common case agentPart is undefined and no filtering happens, so groupIds becomes a no-op and quota/budget checks aggregate unrelated sessions, producing incorrect enforcement decisions.
Useful? React with 👍 / 👎.
Summary
Describe the problem and fix in 2–5 bullets:
Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
List user-visible changes (including defaults/config).
If none, write
None.Security Impact (required)
Yes/No)Yes/No)Yes/No)Yes/No)Yes/No)Yes, explain risk + mitigation:Repro + Verification
Environment
Steps
Expected
Actual
Evidence
Attach at least one:
Human Verification (required)
What you personally verified (not just CI), and how:
Compatibility / Migration
Yes/No)Yes/No)Yes/No)Failure Recovery (if this breaks)
Risks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write
None.