* fix(hooks): suppress main session events for silent/delivered hook turns
When a hook agent turn returns NO_REPLY (SILENT_REPLY_TOKEN), mark the
result as delivered so the hooks handler skips enqueueSystemEvent and
requestHeartbeatNow. Without this, every Gmail notification classified
as NO_REPLY still injects a system event into the main agent session,
causing context window growth proportional to email volume.
Two-part fix:
- cron/isolated-agent/run.ts: set delivered:true when synthesizedText
matches SILENT_REPLY_TOKEN so callers know no notification is needed
- gateway/server/hooks.ts: guard enqueueSystemEvent + requestHeartbeatNow
with !result.delivered (addresses duplicate delivery, refs #20196)
Refs: https://github.com/openclaw/openclaw/issues/20196
* Changelog: document hook silent-delivery suppression fix
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(gateway): avoid premature agent.wait completion on transient errors
* fix(agent): preemptively guard tool results against context overflow
* fix: harden tool-result context guard and add message_id metadata
* fix: use importOriginal in session-key mock to include DEFAULT_ACCOUNT_ID
The run.skill-filter test was mocking ../../routing/session-key.js with only
buildAgentMainSessionKey and normalizeAgentId, but the module also exports
DEFAULT_ACCOUNT_ID which is required transitively by src/web/auth-store.ts.
Switch to importOriginal pattern so all real exports are preserved alongside
the mocked functions.
* pi-runner: guard accumulated tool-result overflow in transformContext
* PI runner: compact overflowing tool-result context
* Subagent: harden tool-result context recovery
* Enhance tool-result context handling by adding support for legacy tool outputs and improving character estimation for message truncation. This includes a new function to create legacy tool results and updates to existing functions to better manage context overflow scenarios.
* Enhance iMessage handling by adding reply tag support in send functions and tests. This includes modifications to prepend or rewrite reply tags based on provided replyToId, ensuring proper message formatting for replies.
* Enhance message delivery across multiple channels by implementing sticky reply context for chunked messages. This includes preserving reply references in Discord, Telegram, and iMessage, ensuring that follow-up messages maintain their intended reply targets. Additionally, improve handling of reply tags in system prompts and tests to support consistent reply behavior.
* Enhance read tool functionality by implementing auto-paging across chunks when no explicit limit is provided, scaling output budget based on model context window. Additionally, add tests for adaptive reading behavior and capped continuation guidance for large outputs. Update related functions to support these features.
* Refine tool-result context management by stripping oversized read-tool details payloads during compaction, ensuring repeated read calls do not bypass context limits. Introduce new utility functions for handling truncation content and enhance character estimation for tool results. Add tests to validate the removal of excessive details in context overflow scenarios.
* Refine message delivery logic in Matrix and Telegram by introducing a flag to track if a text chunk was sent. This ensures that replies are only marked as delivered when a text chunk has been successfully sent, improving the accuracy of reply handling in both channels.
* fix: tighten reply threading coverage and prep fixes (#19508) (thanks @tyler6204)
When an isolated cron session has no lastAccountId (e.g. first-run or
fresh session), the message tool receives an undefined accountId which
defaults to "default". In multi-account setups where accounts are named
(e.g. "willy", "betty"), this causes resolveTelegramToken() to fail
because accounts["default"] doesn't exist.
This change adds a fallback in resolveDeliveryTarget(): when the
session-derived accountId is undefined, look up the agent's bound
account from the bindings config using buildChannelAccountBindings().
This mirrors the same binding resolution used for inbound routing,
closing the gap between inbound and outbound account resolution.
Session-derived accountId still takes precedence when present.
Fixes#17889
Related: #12628, #16259
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses Greptile review comment: when !isNewSession, the spread already
copies all entry fields. The explicit entry?.field assignments were
redundant and could cause confusion. Simplified to only override the
core fields (sessionId, updatedAt, systemSent).
When a webhook or cron job provides a stable sessionKey, the session
should maintain conversation history across invocations. Previously,
resolveCronSession always generated a new sessionId and hardcoded
isNewSession: true, preventing any conversation continuity.
Changes:
- Check if existing entry has a valid sessionId
- Evaluate freshness using configured reset policy
- Reuse sessionId and set isNewSession: false when fresh
- Add forceNew parameter to override reuse behavior
- Spread existing entry to preserve conversation context
This enables persistent, stateful conversations for webhook-driven
agent endpoints when allowRequestSessionKey is configured.
Fixes#18027
When an agent config specifies `model: { primary: "..." }` without
an explicit `fallbacks` array, the existing code replaced the entire
model object from `agents.defaults`—discarding the default fallbacks.
This caused cron jobs (and agent sessions) to have only one model
candidate (the pinned model) plus the global primary as a final
fallback, skipping all intermediate fallback models.
The fix merges the agent model override into the existing defaults
model object using spread, so that keys like `fallbacks` survive
when the agent only overrides `primary`. Agents can still explicitly
override or clear fallbacks by providing their own `fallbacks` array.
Reproduction scenario:
- `agents.defaults.model = { primary: "codex", fallbacks: ["opus", "flash", "deepseek"] }`
- Agent config: `model: { primary: "codex" }`
- Cron job pins: `model: "flash"`
- Before fix: fallback candidates = [flash, codex] (3 models lost)
- After fix: fallback candidates = [flash, opus, deepseek, ..., codex]