The Telegram channel adapter listed no 'poll' action, so agents could
not create polls via the unified action interface. The underlying
sendPollTelegram function was already implemented but unreachable.
Changes:
- telegram.ts: add 'poll' to listActions (enabled by default via gate),
add handleAction branch that reads pollQuestion/pollOption params and
delegates to handleTelegramAction with action 'sendPoll'.
- telegram-actions.ts: add 'sendPoll' handler that validates question,
options (≥2), and forwards to sendPollTelegram with threading, silent,
and anonymous options.
- actions.test.ts: add test verifying poll action routes correctly.
Fixes#16977
When a depth-2 subagent (Birdie) completes and its parent (Newton) is a
depth-1 subagent, the announce should go to Newton, not bypass to the
grandparent (Jaris).
Previously, isSubagentSessionRunActive(Newton) returned false because
Newton's agent turn completed after spawning Birdie. This triggered the
fallback to grandparent even though Newton's SESSION was still alive and
waiting for child results.
Now we only fallback to grandparent if the parent SESSION is actually
deleted (no sessionId in session store). If the parent session exists,
we inject into it even if the current run has ended — this starts a new
agent turn to process the child result.
Fixes#18037
Test Plan:
- Added regression test: routes to parent when run ended but session alive
- Added regression test: falls back to grandparent only when session deleted
- Add readWorkspaceContextForSummary() to extract Session Startup + Red Lines from AGENTS.md
- Inject workspace context into compaction summary (limited to 2000 chars)
- Export extractSections() from post-compaction-context.ts for reuse
- Ensures compaction summary includes core rules needed for recovery
Part 1 of post-compaction context injection feature.
recordAssistantUsage accumulated cacheRead across the entire multi-turn
run, and totalTokens was clamped to contextTokens. This caused
session_status to report 100% context usage regardless of actual load.
Changes:
- run.ts: capture lastTurnTotal from the most recent model call and
inject it into the normalized usage before it reaches agentMeta.
- usage-reporting.test.ts: verify usage.total reflects current turn,
not accumulated total.
Fixes#17016
When a model API call hangs indefinitely (e.g. Anthropic quota exceeded
mid-call), the gateway acquires a session .jsonl.lock but the promise
never resolves, so the try/finally block never reaches release(). Since
the owning PID is the gateway itself, stale detection cannot help —
isPidAlive() always returns true.
This commit adds four layers of defense:
1. **In-process lock watchdog** (session-write-lock.ts)
- Track acquiredAt timestamp on each held lock
- 60-second interval timer checks all held locks
- Auto-releases any lock held longer than maxHoldMs (default 5 min)
- Catches the hung-API-call case that try/finally cannot
2. **Gateway startup cleanup** (server-startup.ts)
- On boot, scan all agent session directories for *.jsonl.lock files
- Remove locks with dead PIDs or older than staleMs (30 min)
- Log each cleaned lock for diagnostics
3. **openclaw doctor stale lock detection** (doctor-session-locks.ts)
- New health check scans for .jsonl.lock files
- Reports PID status and age of each lock found
- In --fix mode, removes stale locks automatically
4. **Transcript error entry on API failure** (attempt.ts)
- When promptError is set, write an error marker to the session
transcript before releasing the lock
- Preserves conversation history even on model API failures
Closes#18060
Add support for Z.AI's native tool_stream parameter to enable real-time
visibility into model reasoning and tool call execution.
- Automatically inject tool_stream=true for zai/z-ai providers
- Allow disabling via params.tool_stream: false in model config
- Follows existing pattern of OpenRouter and OpenAI wrappers
This enables Z.AI API features described in:
https://docs.z.ai/api-reference#streaming
AI-assisted: Claude (OpenClaw agent) helped write this implementation.
Testing: lightly tested (code review + pattern matching existing wrappers)
Closes#18135
Synchronous hook that lets plugins inspect and optionally block messages
before they are written to the session JSONL file. Primary use case is
private mode... when enabled, the plugin returns { block: true } and the
message never gets persisted.
The hook runs on the hot path (synchronous, like tool_result_persist).
Handlers execute sequentially in priority order. If any handler returns
{ block: true }, the write is skipped immediately. Handlers can also
return a modified message to write instead of the original.
Changes:
- src/plugins/types.ts: add hook name, event/result types, handler map entry
- src/plugins/hooks.ts: add runBeforeMessageWrite() following tool_result_persist pattern
- src/agents/session-tool-result-guard.ts: invoke hook before every originalAppend() call
- src/agents/session-tool-result-guard-wrapper.ts: wire hook runner to the guard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Treat normal process exits (even with non-zero codes) as completed tool results.
This prevents standard exit codes (like grep exit 1) from being surfaced
as 'Tool Failure' warnings in the UI. The exit code is still appended
to the tool output for assistant awareness.
Qwen 3 (and potentially other reasoning-capable models served via Ollama)
returns its final answer in a `reasoning` field with an empty `content`
field. This causes blank/empty responses since OpenClaw only reads `content`.
Changes:
- Add `reasoning?` to OllamaChatResponse message type
- Fall back to `reasoning` when `content` is empty in buildAssistantMessage
- Accumulate `reasoning` chunks during streaming when `content` is empty
This allows Qwen 3 to work correctly both with and without /no_think mode.
downgradeOpenAIReasoningBlocks was only called on model change, but
orphaned reasoning items (e.g. from an aborted stream) can exist without
a model switch and cause a 400 from the OpenAI Responses API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The global `agents.defaults.thinkingDefault` forces a single thinking
level for all models. Users running multiple models with different
reasoning capabilities (e.g. Claude with extended thinking, GPT-4o
without, Gemini Flash with lightweight reasoning) cannot optimise the
thinking level per model.
Add an optional `thinkingDefault` field to `AgentModelEntryConfig` so
each entry under `agents.defaults.models` can declare its own default.
Resolution priority: per-model → global → catalog auto-detect.
Example config:
"models": {
"anthropic/claude-sonnet-4-20250514": { "thinkingDefault": "high" },
"openai/gpt-4o": { "thinkingDefault": "off" }
}
Co-authored-by: Cursor <cursoragent@cursor.com>
Add automatic llms.txt awareness so agents check for /llms.txt or
/.well-known/llms.txt when exploring new domains.
Changes:
- System prompt: new 'llms.txt Discovery' section (full mode only,
when web_fetch is available) instructing agents to check for llms.txt
files when visiting new domains
- web_fetch tool: updated description to mention llms.txt discovery
llms.txt is an emerging standard (like robots.txt for AI) that helps
site owners describe how AI agents should interact with their content.
Making this a default behavior helps the ecosystem adopt agent-native
web experiences.
Ref: https://llmstxt.org
When a user sets `agents.defaults.model.primary: "ollama/gemma3:4b"`
but forgets to set OLLAMA_API_KEY, the error is a confusing
"unknown model: ollama/gemma3:4b". The Ollama provider requires any
dummy API key to register (the local server doesn't actually check it),
but this isn't obvious from the error.
Add `buildUnknownModelError()` that detects known local providers
(ollama, vllm) and appends an actionable hint with the env var name
and a link to the relevant docs page.
Before: Unknown model: ollama/gemma3:4b
After: Unknown model: ollama/gemma3:4b. Ollama requires authentication
to be registered as a provider. Set OLLAMA_API_KEY="ollama-local"
(any value works) or run "openclaw configure".
See: https://docs.openclaw.ai/providers/ollamaCloses#17328
Add a `spawn` action to the /subagents command handler that invokes
spawnSubagentDirect() to deterministically launch a named subagent.
Usage: /subagents spawn <agentId> <task> [--model <model>] [--thinking <level>]
Also includes the shared subagent-spawn module extraction (same as the
refactor/extract-shared-subagent-spawn branch) since it hasn't merged yet.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use stricter regex: /^[A-Za-z0-9+/]*={0,2}$/ ensures = only at end
- Normalize URL-safe base64 to standard (- → +, _ → /)
- Added tests for padding in wrong position and URL-safe normalization
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds explicit base64 format validation in sanitizeContentBlocksImages()
to prevent invalid image data from being sent to the Anthropic API.
The Problem:
- Node's Buffer.from(str, "base64") silently ignores invalid characters
- Invalid base64 passes local validation but fails at Anthropic's stricter API
- Once corrupted data persists in session history, every API call fails
The Fix:
- Add validateAndNormalizeBase64() function that:
- Strips data URL prefixes (e.g., "data:image/png;base64,...")
- Validates base64 character set with regex
- Checks for valid padding (0-2 '=' chars)
- Validates length is proper for base64 encoding
- Invalid images are replaced with descriptive text blocks
- Prevents permanent session corruption
Tests:
- Rejects invalid base64 characters
- Strips data URL prefixes correctly
- Rejects invalid padding
- Rejects invalid length
- Handles empty data gracefully
Closes#18212
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Follow-up to #18066 — three session file write sites were missed:
- auto-reply/reply/session.ts: forked session transcript header
- pi-embedded-runner/session-manager-init.ts: session file reset
- gateway/server-methods/sessions.ts: compacted transcript rewrite
All now use mode 0o600 consistent with transcript.ts and chat.ts.
When no embedding provider is available (e.g., OAuth mode without API keys),
memory_search now falls back to FTS-only mode instead of returning disabled: true.
Changes:
- embeddings.ts: return null provider with reason instead of throwing
- manager.ts: handle null provider, use FTS-only search mode
- manager-search.ts: allow searching all models when provider is undefined
- memory-tool.ts: expose search mode in results
The search results now include a 'mode' field indicating 'hybrid' or 'fts-only'.
Add the missing extraArgs property to buildSandboxBrowserResolvedConfig
to satisfy the ResolvedBrowserConfig type, and fix import ordering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>