fix(cron): channel "last" fallback for unconfigured and cross-channel delivery#19034
fix(cron): channel "last" fallback for unconfigured and cross-channel delivery#19034tangopium wants to merge 7 commits intoopenclaw:mainfrom
Conversation
…target When channel "last" resolved to an unconfigured channel, the second resolveSessionDeliveryTarget call passed requestedChannel: "last", causing the function to re-resolve to the stale lastChannel and ignore the fallbackChannel parameter. The caller then shadowed the stale value, making the fix fragile. Pass requestedChannel: fallbackChannel so the function resolves the correct channel internally. Omit allowMismatchedLastTo from the fallback call to prevent cross-channel `to` leakage (e.g. a WhatsApp E.164 number reaching the Telegram API). Fixes openclaw#17905
…nconfigured (openclaw#17905) Cron jobs with `delivery: { channel: "last" }` failed when the session's last channel was not configured (e.g. whatsapp removed) or had no deliverable session history (e.g. last provider was webchat). In run.ts, loosen the no-target early-return so the announce flow can proceed when synthesized text is available but no direct `to` was resolved. Gate the direct-delivery path on `resolvedDelivery.to` to prevent it from running without a target. Add tests covering both scenarios (no session history, unconfigured channel) in normal and best-effort modes.
…ats (openclaw#17905) When a cron job triggers a heartbeat for a non-default agent, runHeartbeatOnce skipped it because isHeartbeatEnabledForAgent returned false (only the default agent or agents with explicit heartbeat config pass that check). Detect cron-triggered runs via the "cron:" reason prefix and bypass the agent-enabled and interval checks, since the cron scheduler is the authority on when these heartbeats should fire.
…nclaw#17905) Verify that cron enqueues system events with the correct agentId for non-default agents, and that the heartbeat runner processes them when triggered via cron (bypassing the agent-enabled check).
| } catch { | ||
| // resolveMessageChannelSelection failed (e.g. no configured accounts). | ||
| // Check plugin registry directly — if the session channel has no | ||
| // registered plugin, fall back to the first available outbound plugin. | ||
| if (!getChannelPlugin(preliminary.channel)) { | ||
| const alt = listChannelPlugins().find((p) => p.outbound); | ||
| if (alt) { | ||
| fallbackChannel = alt.id as Exclude<OutboundChannel, "none">; | ||
| } | ||
| } |
There was a problem hiding this comment.
Plugin registry vs. configuration check
The catch block here handles the case where resolveMessageChannelSelection throws. One reason it throws is zero configured channels, but another is multiple configured channels (see channel-selection.ts:89). In the multiple-channels case, preliminary.channel might reference an unconfigured channel (e.g. Signal removed), but getChannelPlugin checks the plugin registry (all registered plugins, regardless of account configuration), not whether the channel has a valid configured account. So if a channel has a registered plugin but no configured account, this branch won't trigger the fallback.
This is a narrow edge case (multiple configured channels + unconfigured session channel + plugin still registered), so it's not blocking, but worth noting for future robustness. A potential improvement would be to check against listConfiguredMessageChannels or similar instead of getChannelPlugin.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cron/isolated-agent/delivery-target.ts
Line: 68:77
Comment:
**Plugin registry vs. configuration check**
The catch block here handles the case where `resolveMessageChannelSelection` throws. One reason it throws is zero configured channels, but another is _multiple_ configured channels (see `channel-selection.ts:89`). In the multiple-channels case, `preliminary.channel` might reference an unconfigured channel (e.g. Signal removed), but `getChannelPlugin` checks the _plugin registry_ (all registered plugins, regardless of account configuration), not whether the channel has a valid configured account. So if a channel has a registered plugin but no configured account, this branch won't trigger the fallback.
This is a narrow edge case (multiple configured channels + unconfigured session channel + plugin still registered), so it's not blocking, but worth noting for future robustness. A potential improvement would be to check against `listConfiguredMessageChannels` or similar instead of `getChannelPlugin`.
How can I resolve this? If you propose a fix, please make it concise.|
@greptileai please review |
1027b78 to
daea50d
Compare
daea50d to
9b1d833
Compare
# Conflicts: # src/cron/isolated-agent/run.ts
Summary
"last"fallback indelivery-target.tssoresolveSessionDeliveryTargetresolves the correct fallback channel internally, rather than relying on the caller to shadow a stale valueallowMismatchedLastTofrom the fallback call to prevent cross-channeltoleakage (e.g. a WhatsApp E.164 number reaching the Telegram API)run.tsso the announce flow can proceed when synthesized text is available but no directtowas resolvedheartbeat-runner.tsfor cron-triggered heartbeats, so non-default agents can runTest plan
src/cron/isolated-agent.channel-last-resolution.test.ts— 4 new tests (no history, unconfigured channel, normal + best-effort)src/cron/isolated-agent/delivery-target.test.ts— 5 existing tests passsrc/cron/service.runs-one-shot-main-job-disables-it.test.ts— 13 tests pass (1 new)src/cron/service.non-default-agent-main-job.test.ts— 3 new testssrc/infra/heartbeat-runner.returns-default-unset.test.ts— 35 tests pass (2 new)Fixes #17905
Greptile Summary
Fixes cross-channel delivery leakage and fallback resolution for cron jobs using channel
"last", and enables cron-triggered heartbeats for non-default agents.delivery-target.ts: Adds verification that a session's"last"channel is actually configured before using it. When it isn't (e.g. WhatsApp removed from config but still referenced in session), falls back to a configured channel. The fallback call now passesrequestedChannel: fallbackChannelinstead of"last", and omitsallowMismatchedLastToto prevent cross-channeltoleakage (e.g. a WhatsApp E.164 number reaching Telegram).run.ts: Loosens the no-target early return so the announce flow can proceed whensynthesizedTextis available but no directtowas resolved. Adds aresolvedDelivery.toguard before attempting direct outbound delivery, ensuring the code falls through to the announce path when there's no target address.heartbeat-runner.ts: Bypasses theisHeartbeatEnabledForAgentandresolveHeartbeatIntervalMschecks when the heartbeat is triggered by cron (reason starts with"cron:"), allowing non-default agents to run cron-scheduled heartbeats even without explicit heartbeat config.Confidence Score: 4/5
src/cron/isolated-agent/delivery-target.ts— the catch block fallback path (lines 68-78) uses plugin registry rather than configured account checks, a trade-off already noted in prior review discussion.Last reviewed commit: 6213a85