- Add OPENROUTER_SHORT_TO_API_MODEL_ID map for short model refs like
openrouter/deepseek-v4-flash that OpenClaw surfaces but OpenRouter API
expects as deepseek/deepseek-v4-flash.
- In normalizeOpenRouterApiModelId, expand short refs before falling back
to the existing namespaced strip logic.
- Add unit tests covering short refs, long refs, native routes, and
pass-through cases.
- Add standalone reproduction script that verifies all normalization cases.
Voice/audio messages sent to Feishu (opus) play fine but show no duration
on the bubble. Feishu derives the voice-bubble duration from the `duration`
parameter of the file upload API (`im/v1/files`); the audio message content
only carries `{file_key}` and has no duration field, so the duration was
never set.
`sendMediaFeishu` now probes the outgoing audio with `ffprobe` and passes the
result as the upload `duration` (ms). It probes the buffer that is actually
sent (after the existing voice transcode, which caps length via
`MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS`), so the reported length matches what
is played. Probing is best-effort: on failure it logs and omits the duration,
and the message still sends. The audio message content is unchanged.
Fixes#53798
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The persisted iMessage echo-dedupe cache normalized text with CRLF->LF + trim only, not the leading attributedBody corruption-marker stripping the in-memory echo cache applies (#93511). The persisted 12h cache is the only matcher once the 4s in-memory text TTL expires, so a delayed reflected own-message echo whose text decoded with a leading NUL/replacement/BOM marker did not match the clean stored send -- the agent's own message was re-ingested as fresh inbound, causing a self-reply loop.
Extract the marker-stripping into a leaf module shared by both echo caches (the in-memory cache already imports the persisted one, so importing back would be a cycle) and apply it in the persisted normalizeText, so both caches strip identically.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
oxlint(curly) rejected the bare `if (!params) continue;` continue inside
the regression test added for #86034. Wrap the body in braces. No logic
change.
Locks the mergeDeliveryContext channelsConflict guard so a stale lastChannel that differs from the completion origin's channel cannot import its lastTo. Addresses ClawSweeper's contract question on PR #89949.
node_modules not available in this worktree; vitest was not run locally. CI is the gate.
When a media-generation task is created off the direct-reply path (heartbeat,
cron, subagent spawn), `agentTo` is undefined and the persisted
`requesterOrigin` lacks `to`. Every downstream `Boolean(channel && to)` gate
then short-circuits, so the generated artifact is never delivered even though
the artifact exists on disk and `task_runs.status` is later marked failed with
`completion delivery failed after successful generation`.
The requester session entry already carries `lastTo`/`lastChannel`/
`lastAccountId` and is loaded in the same function further down. Merge that
context back into `effectiveDirectOrigin` before the deliverability decision,
as the existing comment at the same site already promises.
Fixes#86034 (Hypothesis A). Hypothesis B (wake-false skips direct fallback)
remains a separate follow-up - see issue thread for details.
Both `openclaw configure` and the no-subcommand `openclaw config` route through `configureCommandFromSectionsArg`, so a single guard there fail-closes both entry points when stdin/stdout are not TTYs instead of partially entering the wizard and exiting dirty (exit 13) on a piped stdin.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(google): add gemini-3.5-flash model catalog entry
gemini-3.5-flash was missing from the bundled Google model catalog,
causing it to silently fall back to DEFAULT_CONTEXT_TOKENS (200k)
instead of its documented 1,048,576-token input window.
Add the catalog entry and forward-compat routing so the model
resolves with the correct context window.
Closes: openclaw/openclaw#94723
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: retry CI (flaky test)
---------
Co-authored-by: Claude <noreply@anthropic.com>
* perf(plugins): cache existence probes within bundle manifest scan
Bundle plugin discovery re-probes the same marker paths (skills/, commands/,
agents/, .mcp.json, .lsp.json, settings.json, hooks/hooks.json) once in
detectBundleManifestFormat and again in loadBundleManifest's capability
builders. Across the bundled plugin tree this is thousands of redundant
synchronous fs.existsSync calls; #76209 reports 25.4s of self-time on a
Windows cold start.
Add a scan-scoped existence cache (plugin-scan-existence-cache.ts) entered
only around discoverBundleInRoot. pluginScanExistsSync memoizes inside the
active scan and falls back to plain fs.existsSync outside it, so install,
hooks, and doctor flows stay uncached. The cache is push/pop per
discoverBundleInRoot call (try/finally), so a later install/repair pass
re-reads the filesystem — no process-global staleness.
Measured on Windows over a 25-plugin fixture: 550 -> 325 fs.existsSync
calls (41% fewer), 294.75ms -> 208.49ms. Discovery results unchanged.
Closes#76209
* fix(plugins): drop unused test reset helper and satisfy oxlint
Remove __resetPluginScanExistenceCacheForTest: the scan cache is push/pop
balanced by try/finally in withPluginScanExistenceCache, so the stack never
leaks between tests and the helper was dead code. It also tripped oxlint
no-underscore-dangle. Refactor the integration test to count existsSync calls
via a const-returning helper so there is no useless assignment.
* fix(sessions): keep bound channel identity across non-delivery turns
mergeOrigin reset channel-keyed origin fields (nativeChannelId,
nativeDirectUserId, accountId, threadId) whenever the new turn's
provider/surface/account differed, intended for a real Slack -> Telegram
switch. A non-delivery turn (gateway webchat send, heartbeat/cron/webhook
tick) derives origin.provider as the internal channel, so it was treated
as a channel switch and wiped the session's live channel/thread identity
even though the session never left that channel.
Gate the reset on the new turn being a real, deliverable channel so
internal non-delivery turns preserve the bound channel identity while a
genuine cross-channel switch still resets it.
* fix(sessions): also exclude system-event providers from the channel-switch reset
cron-event and exec-event turns (and heartbeat) carry no channel of their
own. They can reach mergeOrigin through the non-skip callers
(recordSessionMetaFromInbound / updateLastRoute) that derive an origin
without skipSystemEventOrigin, so the channel-switch reset would wipe a
bound session's native channel/thread identity. Add isSystemEventProvider
to the non-deliverable gate (reusing it to de-dupe the same check already
inlined in deriveSessionOrigin).
`isTableSeparatorLine` required 3+ dashes per cell (`/^:?-{3,}:?$/`), but a
GFM delimiter cell needs only one or more dashes. So a valid table whose
separator used 1 or 2 dashes (e.g. `|--|--|`) was not recognized: the header
stayed pending and was silently overwritten by each following row, so the
table's header, separator, and every row but the last vanished from the sent
message.
Accept `-+` so valid GFM separators are recognized, matching the spec and the
sibling LINE channel. Every existing test separator already uses 3+ dashes, so
they are byte-identical.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>