* fix(browser): resolve act targetId aliases before mismatch check
The /act top-level and batch targetId guards compared the caller-supplied
targetId against the resolved canonical tab.targetId with raw string
equality. Any supported alias form (tabId, label, suggestedTargetId, or a
unique id prefix) resolves to a different canonical id, so act requests that
followed the documented 'prefer suggestedTargetId/tabId/label' guidance were
rejected with 403 ACT_TARGET_ID_MISMATCH even though they named the correct
tab. snapshot/open/close/tabs lack this guard and kept working, matching the
reported symptom matrix.
Resolve the action targetId through the same tab alias resolution the route
used and reject only ids that resolve to a different tab.
* fix(browser): canonicalize act targetId aliases before Playwright dispatch
The /act gate accepted tabId/label/suggested/prefix aliases of the request
tab but left them on action.targetId. The managed executor reads
action.targetId ?? targetId for an exact page lookup (executeSingleAction ->
getPageForTargetId), so an alias missed the lookup and broke the action at
runtime whenever more than one page was open (single-page masked it via the
pages.length===1 fallback). Canonicalize the action targetId (top-level and
nested batch sub-actions) to the resolved tab id before dispatch; reject ids
that resolve to a different tab. Replace the mock-masked contract assertions
with executor-action assertions plus direct canonicalizer unit tests.
* test(browser): brace act-targetId guard clauses for curly lint
* docs(changelog): note browser target alias fix
* docs(changelog): note browser target alias fix
* fix(browser): reject ambiguous batch target aliases
* test(browser): type targetless act fixture
* docs(changelog): move browser alias fix to unreleased
* chore: drop nonessential browser changelog entry
---------
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cron): preserve delivery thread id type across SQLite reload
A numeric delivery threadId such as a Telegram forum topic id reloaded as
a string after a gateway restart because the split delivery_thread_id
column stores TEXT. resolveCronDeliveryPlan and channel delivery are type
sensitive, so the numeric topic id was forwarded as a string and dropped
the configured topic.
Source the delivery threadId type from the canonical job_json config on
read, falling back to the raw column text. Both freshly written rows and
legacy bare-text rows keep string versus number identity without any
column format change or migration.
* fix(cron): recover typed delivery thread id when split column is null
* fix(cron): persist delivery thread ID type
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): surface real plugin approval rejection reason to agent
Distinguish gateway-rejection errors from transport failures in the
plugin approval catch block. When the error is a GatewayClientRequestError
(with a structured gatewayCode), the gateway is reachable and actively
rejected the request — surface the real rejection reason. Otherwise keep
the existing "gateway unavailable" message for genuine transport failures.
Previously every approval gateway error was reported as "gateway
unavailable", even when the gateway was healthy and returned a
structured rejection like INVALID_REQUEST. This misdirected operators
and agents toward connectivity debugging instead of the actual schema
or policy violation.
Fixes#100212
* fix(agents): report plugin approval rejections accurately
Co-authored-by: 唐梓夷0668001293 <tang.ziyi@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Consume the gateway's canonical reconnect snapshot (chat.history inFlightRun)
to re-adopt still-streaming runs after reconnect, cold start, and seq-gap
recovery in the Android ChatController. Mirrors the iOS contract from #100277.
Part of #100197.
* fix(irc): chunk PRIVMSG on UTF-16 boundary to avoid lone surrogates
sendPrivmsg split long messages with String.slice on a UTF-16 code-unit
index, so an emoji (or other astral character) straddling the split
point was cut into a lone high/low surrogate, sending broken bytes in
the PRIVMSG to the IRC server.
Slice each chunk with sliceUtf16Safe so a surrogate pair is never split.
When the budget is too small to fit even the leading astral character,
emit that character whole so chunking still makes progress.
Adds a socket-level test (fake net/tls socket) asserting no PRIVMSG
chunk contains a lone surrogate, including the one-code-unit budget
edge case, while the existing space-preferring split is preserved.
* refactor(irc): make surrogate-safe chunking direct
* fix(irc): preserve one-unit chunk limits
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(voice-call): resolve completed calls from the persisted store on status misses
get_status, the legacy status mode, and the voicecall.status gateway method
only consulted the in-memory call manager. Once a call was evicted (finalize,
gateway restart, or max-duration expiry) they reported { found: false } even
though the full record remained on disk. Fall back to the persisted call
history and resolve the NEWEST matching snapshot — history is oldest-first, so
a forward find() returns a stale record (the regression in the prior attempt).
Closes#96586
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(voice-call): use bracket access for mocked getCallHistory assertion
Avoids the typescript(unbound-method) lint rule that flags referencing a
typed method (`runtimeStub.manager.getCallHistory`) as an unbound value.
Bracket access matches the existing mock-assertion pattern in this file
(e.g. `runtimeStub.manager["sendDtmf"]`).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: re-trigger QA Smoke (transient build-OOM, also failing main run 28695162780)
* fix(voice-call): resolve persisted calls in the CLI status fallback too
The local CLI `voicecall status` gateway-unavailable fallback only consulted
the in-memory manager, so a completed/evicted call still returned
{ found: false } even though the gateway/tool/legacy status paths now fall
back to the persisted store. Align this fourth status reader: consult
getCallByProviderCallId in addition to getCall, and on an active miss resolve
the NEWEST matching persisted snapshot via getCallHistory(100) +
toReversed().find(...) (history is oldest-first), mirroring the gateway/tool
paths. Return shape is unchanged.
Per review on #96586 (align CLI status before merge).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(voice-call): centralize persisted status lookup
Co-authored-by: 曾文锋0668000834 <zeng.wenfeng@xydigit.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(logbook): automatic work journal plugin with a plugin-contributed Control UI tab
Squash of PR #99930 work for rebase onto the Control UI route refactor:
- extensions/logbook: Dayflow-style capture -> observations -> timeline cards
pipeline with SQLite store, node capture commands, standup/ask, retention
- plugin SDK/gateway seam: surface "tab" Control UI descriptors projected
into hello-ok controlUiTabs (scope-filtered, deterministic order)
- Control UI: dynamic plugin tabs with bundled Logbook view
- docs, tests, labeler wiring
* feat(ui): port plugin tabs and Logbook to the route-owned Control UI architecture
- shared /plugin route carries the tab id in the query (?id=<tab>), matching
the router's exact-path contract
- openclaw-plugin-page renders bundled views (Logbook), sandboxed plugin
frames (descriptor path), or the unavailable card
- sidebar renders hello controlUiTabs after each group's static routes
- Logbook view/controller live under ui/src/pages/plugin/
* fix(ui): namespace plugin tabs by pluginId to prevent cross-plugin tab id collisions
* fix(logbook): prefer app capture nodes and rotate off failing nodes
* fix(plugins): reject protocol-relative Control UI tab paths
* fix(logbook): harden automatic journal
* docs(changelog): remove maintainer self-credit
* chore(ui): refresh locale metadata after rebase
* fix(logbook): preserve analysis window boundaries
* fix(logbook): align status privacy and timezone
* fix(ui): stop hidden plugin tab polling
* fix(cron): reject sub-millisecond durations
* fix(skill-workshop): preserve proposal terminal newline
Preserve proposal_content exactly at the agent tool boundary and make
renderProposalMarkdown defensively emit a terminal newline.
Add focused regressions for the tool write path and markdown renderer.
* fix(skill-workshop): reject blank raw proposal content
* fix: treat empty-string optional integer tool params as unset
Optional positive-integer tool params (e.g. Telegram replyTo/threadId)
threw ToolInputError when a tool-calling model populated them with an
empty-string or whitespace-only default. Those defaults carry no value,
so readPositiveIntegerParam/readNonNegativeIntegerParam now treat a
blank string as unset (undefined) instead of throwing, while still
rejecting genuinely invalid present values (0, "42.5", "-3"). This
prevents silent message-delivery failures when models emit empty
routing-param defaults. Adds unit tests covering blank vs invalid.
* fix(gateway): log start session persistence failures
The gateway session-lifecycle "start" event persistence
(persistGatewaySessionLifecycleEvent, which surfaces write failures via
requireWriteSuccess) was fired as void ...catch(() => undefined), swallowing
the rejection with zero logging. A failed start-marker write silently dropped
the run's start record from restart-recovery accounting, with no
operator-visible trace.
The sibling terminal-phase catch already logs this since #97839; the start
path was the unfixed sibling. Mirror that fix: log the swallowed start-phase
persistence failure with the same redacted message shape via formatForLog,
keeping the fire-and-forget semantics unchanged. Adds a focused regression
test asserting the log fires on start-persist rejection.
* fix(memory): report close-time pending work failures
* fix(shared): return "" from sliceUtf16Safe when end <= start, matching native .slice
sliceUtf16Safe silently swapped reversed bounds (to < from) instead of
returning "" like String.prototype.slice, creating a subtle footgun for
callers with dynamic start/end pairs.
Caller scan across src/, extensions/, and packages/ confirmed no production
code relies on the old swap behavior — all callers use (text, 0, N) or
(text, -N) forms only.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(plugins): require plugin manifest in npm verifier
* fix(android): propagate CancellationException in CameraHandler catch blocks
Catch (err: Throwable) swallows kotlinx.coroutines.CancellationException,
breaking structured concurrency when the coroutine scope is cancelled
during camera operations (handleList/handleSnap/handleClip).
Add CancellationException rethrow before each Throwable catch to match
the existing pattern used in GatewaySession and TalkModeManager.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(android): propagate CancellationException past GatewaySession invoke boundary
* chore: sync native i18n inventory after gateway session line shift
* fix(agents): prevent native hook relay bridge race condition on renew and registration
Remove synchronous bridge record write after server.listen() that races
before the TCP server binds, and guard renew handler with server.listening
check to prevent stale relay registrations.
Closes#98650
* fix: harden small reliability fixes
Co-authored-by: cxbAsDev <cxbAsDev@users.noreply.github.com>
* docs(agents): explain buffered LSP spawn failures
* docs(agents): clarify LSP spawn timeout invariant
---------
Co-authored-by: qingminlong <qing.minlong@xydigit.com>
Co-authored-by: anyech <anyech@gmail.com>
Co-authored-by: snotty <snotty@users.noreply.github.com>
Co-authored-by: masatohoshino <g515hoshino@gmail.com>
Co-authored-by: lin-hongkuan <lin-hongkuan@users.noreply.github.com>
Co-authored-by: simon-w <weng.qimeng@xydigit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: 宇宙熊Yzx <53250620+849261680@users.noreply.github.com>
Co-authored-by: xialonglee <li.xialong@xydigit.com>
Co-authored-by: nankingjing <1079826437@qq.com>
Co-authored-by: cxbAsDev <cxbAsDev@users.noreply.github.com>