#17070: fix(telegram): Outbound: ignore empty legacy target fields
stale
size: S
Cluster:
Message Handling Improvements
## Summary
- Problem: `message` tool calls could include `target` plus empty legacy fields (`to: ""` / `channelId: ""`), and `applyTargetToParams` treated any string (including empty) as legacy conflict, throwing `Use \`target\` instead of \`to\`/\`channelId\`.`.
- Why it matters: image/file sends failed before dispatch on multiple channels (reported on telegram), even when `target` was valid. #5364
- What changed: in `src/infra/outbound/channel-target.ts`, legacy-field detection now only triggers for **non-empty** `to/channelId`; strict rejection for non-empty legacy usage is preserved.
- What did NOT change (scope boundary): no channel plugin behavior changed; no media hydration, routing, or gateway transport logic changed.
## Change Type (select all)
- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Docs
- [ ] Security hardening
- [ ] Chore/infra
## Scope (select all touched areas)
- [x] Gateway / orchestration
- [x] Skills / tool execution
- [ ] Auth / tokens
- [ ] Memory / storage
- [x] Integrations
- [ ] API / contracts
- [ ] UI / DX
- [ ] CI/CD / infra
## Linked Issue/PR
#5364
#5608
## User-visible / Behavior Changes
- Sending media/files no longer fails with `Use \`target\` instead of \`to\`/\`channelId\`.` when `target` is present and legacy fields are empty strings.
- Non-empty legacy `to/channelId` is still rejected.
## Security Impact (required)
- New permissions/capabilities? (`No`)
- Secrets/tokens handling changed? (`No`)
- New/changed network calls? (`No`)
- Command/tool execution surface changed? (`No`)
- Data access scope changed? (`No`)
- If any `Yes`, explain risk + mitigation: `N/A`
## Repro + Verification
### Environment
- OS: Linux (Arch)
- Runtime/container: Node `v22.17.1, pnpm openclaw --dev gateway
- Relevant config (redacted): standard local dev config, no special overrides required for repro
### Steps
1. Run `pnpm openclaw --dev gateway --force --verbose`.
2. Trigger a `message` tool call to send image/file with `action: "send"` and valid `target`.
3. Observe payload may contain `channelId: ""` (or `to: ""`) from model-generated optional fields.
### Expected
- Request should proceed using `target` and send normally.
### Actual
- Before fix: throws `Use \`target\` instead of \`to\`/\`channelId\`.` before dispatch.
- After fix: empty legacy fields no longer trip validation; non-empty legacy fields still rejected.
## Evidence
Attach at least one:
- [x] Failing test/log before + passing after
- [x] Trace/log snippets
- [ ] Screenshot/recording
- [ ] Perf numbers (if relevant)
Evidence pointers:
- Failing tool-call payload + error:
```
13:10:38 tools: message failed stack:
Error: Use `target` instead of `to`/`channelId`.
at applyTargetToParams (file:///home/yhw/Develop/openclaw/dist/reply-CX2QH7tO.js:35079:48)
at runMessageAction (file:///home/yhw/Develop/openclaw/dist/reply-CX2QH7tO.js:40119:2)
at execute (file:///home/yhw/Develop/openclaw/dist/reply-CX2QH7tO.js:40486:25)
at execute (file:///home/yhw/Develop/openclaw/dist/reply-CX2QH7tO.js:44602:17)
at processTicksAndRejections (node:internal/process/task_queues:105:5)
at Object.execute (file:///home/yhw/Develop/openclaw/dist/reply-CX2QH7tO.js:44526:11)
at Object.execute (file:///home/yhw/Develop/openclaw/dist/reply-CX2QH7tO.js:47026:21)
at Object.execute (file:///home/yhw/Develop/openclaw/node_modules/.pnpm/@mariozechner+pi-coding-agent@0.52.12_ws@8.19.0_zod@4.3.6/node_modules/@mariozechner/pi-coding-agent/src/core/extensions/wrapper.ts:71:20)
at executeToolCalls (file:///home/yhw/Develop/openclaw/node_modules/.pnpm/@mariozechner+pi-agent-core@0.52.12_ws@8.19.0_zod@4.3.6/node_modules/@mariozechner/pi-agent-core/src/agent-loop.ts:324:13)
at runLoop (file:///home/yhw/Develop/openclaw/node_modules/.pnpm/@mariozechner+pi-agent-core@0.52.12_ws@8.19.0_zod@4.3.6/node_modules/@mariozechner/pi-agent-core/src/agent-loop.ts:157:27)
at file:///home/yhw/Develop/openclaw/node_modules/.pnpm/@mariozechner+pi-agent-core@0.52.12_ws@8.19.0_zod@4.3.6/node_modules/@mariozechner/pi-agent-core/src/agent-loop.ts:51:3
```
## Human Verification (required)
What you personally verified (not just CI), and how:
- Verified scenarios: reproduced failing path from session log payload; confirmed root cause in `applyTargetToParams`; verified updated condition logic and targeted vitest pass.
- Edge cases checked: non-empty legacy `channelId/to` still throws; empty legacy fields do not throw.
- What you did **not** verify: end-to-end live media send on each real channel account after patch.
## Compatibility / Migration
- Backward compatible? (`Yes`)
- Config/env changes? (`No`)
- Migration needed? (`No`)
- If yes, exact upgrade steps: `N/A`
## Failure Recovery (if this breaks)
- How to disable/revert this change quickly: revert commit `f1280266f`.
- Files/config to restore: `src/infra/outbound/channel-target.ts`.
- Known bad symptoms reviewers should watch for: legacy non-empty `to/channelId` no longer being rejected (should still be rejected).
## Risks and Mitigations
- Risk: relaxing legacy detection could accidentally allow malformed legacy input.
- Mitigation: only empty-string legacy values are ignored; non-empty legacy fields continue to hard-fail, preserving strict contract.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Fixes a bug where `applyTargetToParams` in `channel-target.ts` incorrectly threw `"Use \`target\` instead of \`to\`/\`channelId\`."` when model-generated tool calls included a valid `target` alongside empty-string legacy fields (`to: ""` or `channelId: ""`). The fix narrows legacy-field detection to only trigger on **non-empty** strings, which is consistent with how the caller (`runMessageAction` in `message-action-runner.ts`) already handles legacy fields using the same `.trim().length > 0` pattern.
- `src/infra/outbound/channel-target.ts`: `hasLegacyTo` and `hasLegacyChannelId` now require the string value to be non-empty after trimming, matching the existing convention in `runMessageAction` and `actionHasTarget`.
- `appcast.xml`: Routine release metadata update (adds 2026.2.15, removes 2026.2.12).
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge — it's a minimal, well-scoped bug fix that aligns with existing codebase patterns.
- The change is a two-line fix that narrows empty-string detection to match the convention already used by the calling code (`runMessageAction`) and sibling function (`actionHasTarget`). Non-empty legacy fields still trigger the same strict rejection. The logic is straightforward and the risk of regression is very low.
- No files require special attention.
<sub>Last reviewed commit: 87147ee</sub>
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#19399: telegram: fix MEDIA false positives and partial final drop
by HOYALIM · 2026-02-17
81.9%
#23723: feat(message): improve send param ergonomics and actionable routing...
by SmithLabsLLC · 2026-02-22
81.6%
#19143: fix: support target param in message tool send extraction
by botverse · 2026-02-17
79.4%
#19632: fix: suppressToolErrors now suppresses exec tool failure notifications
by Gitjay11 · 2026-02-18
79.1%
#20735: fix: skip auto-attaching tool MEDIA: paths already sent via message t…
by anillBhoi · 2026-02-19
78.1%
#16733: fix(ui): avoid injected newlines when tool output is hidden
by jp117 · 2026-02-15
77.6%
#21271: fix(commands): pass channel/capabilities/shell/os to runtime in com...
by evansantos · 2026-02-19
77.5%
#16996: fix(cron): parse Telegram topic target format for announce delivery
by Glucksberg · 2026-02-15
77.4%
#13489: fix: preserve Slack channel/user ID case in target normalization
by sandieman2 · 2026-02-10
77.3%
#17552: fix(agents): suppress tool error warnings when assistant already re...
by AytuncYildizli · 2026-02-15
77.3%