* fix(gateway): bound busy channel health by real run age
The channel health policy treats a channel as healthy-busy even while
disconnected, bounded only by a 25 minute stale ceiling measured from
lastRunActivityAt. The run-state heartbeat refreshes lastRunActivityAt
every 60 seconds for as long as any run is active, so a run that hangs
forever (for example a send blocking on a dead socket after the
transport already reported connected:false) keeps that timestamp fresh
and the stuck ceiling is never reached. The account is then reported
healthy forever by the health monitor, readiness probe, and health CLI,
and no restart ever fires.
createRunStateMachine now tracks each in-flight run's start time keyed by
an opaque run handle and publishes the oldest still-active run's start as
activeRunStartedAt. The health policy busy override keys its ceiling off
the real run age, so a run stuck longer than the threshold reports stuck
and the monitor can restart it. Because the reported start is the oldest
active run and advances to the next-oldest as runs complete, a channel
churning through many short overlapping runs (activeRuns above 1 across
concurrent queue keys) stays healthy; only a genuinely hung run breaches
the ceiling. Short and active runs stay healthy and the existing
lastRunActivityAt fallback is preserved for snapshots without a start
time.
* fix(channels): retain run-state callback compatibility
Keep the released zero-argument onRunEnd callback source-compatible while allowing internal queue callers to pass a run handle for exact concurrent-run accounting. The compatibility path closes the oldest active run, preserving existing lifecycle behavior for consumers that do not use handles.
* fix(channels): keep anonymous runs out of age tracking
The zero-argument lifecycle callbacks cannot identify which concurrent run completed, so they must not update the identity-sensitive run start used by channel health. Keep their busy count separately and reserve exact start tracking for the shared queue's handle-aware lifecycle path.
* fix(channels): keep tracked runs internal
Keep the public run-state lifecycle callbacks unchanged. The channel queue now owns opaque run identity and augments its status updates with the oldest active queue run, so implementation details do not expand the SDK surface.
* fix(channels): type queue run start status
Keep activeRunStartedAt in the internal status patch type so the queue can publish its private tracked-run age through the existing status sink.
* fix(channels): wrap isActive to satisfy unbound-method lint
* fix(gateway): gate busy run-age ceiling on disconnected transport
Run script payloads through the shared headless code-mode executor with payload-grade budgets and success-only trigger.state persistence.
Reuse cron delivery, wake, pacing, and dangerous trigger-gate contracts for notify, wake, and nextCheck results.
* fix(sessions): commit reduced session index before deleting evicted transcripts
The file-backed session store disk-budget sweep evicted an old session by
removing its in-memory entry and immediately, permanently deleting its
transcript artifact, and only afterwards did the caller serialize and
atomically replace sessions.json. A crash, power loss, or store-write failure
in that window left durable metadata in sessions.json pointing at transcripts
that were already gone, an irreversible loss of evicted session history during
the low-disk maintenance when failures are most likely.
enforceSessionDiskBudget now plans the evicted entries' owned artifact
deletions during the sweep (accounting their freed bytes so the stop condition
is unchanged) and defers the physical unlink until after an injected
commitEvictedIndex callback atomically persists the reduced index. saveSessionStore
supplies that callback. A crash after the commit leaves only reclaimable orphan
files; a crash before it retains the transcript.
* fix(sessions): retain evicted artifacts without commit boundary
* fix(sessions): fsync reduced index before eviction
* fix(transcripts): imported text can inject terminal escapes through transcripts show
Imported transcript text, speaker labels, and session titles were rendered
into summary.md unsanitized, and openclaw transcripts show writes that file
directly to stdout, so a transcript could clear the terminal or spoof
colored status text. Sanitize external strings with the terminal-core
sanitizer when the summary is built, so summary.json, summary.md, and the
CLI output stay free of control bytes while transcript.jsonl keeps the raw
capture.
* fix(transcripts): protect terminal output boundaries
* fix(transcripts): keep canonical session identity separate from terminal presentation
* fix(transcripts): escape C1 control characters in transcripts json output
* fix(googlechat): keep accounts.default streaming when doctor migrates named accounts
* fix(googlechat): canonicalize migrated streaming
Replace the layered doctor migration with canonical root/default/account materialization and prove runtime resolution matches the stored config.\n\nCo-authored-by: tiffanychum <71036662+tiffanychum@users.noreply.github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(line): retry inbound media through the ingress drain on transient download failure
* fix(line): retry all transient inbound media failures
Co-authored-by: 許元豪 <146086744+edenfunf@users.noreply.github.com>
* style(line): format media retry test
* test(line): use scanner-safe quote token
* fix(line): narrow durable media retries
* fix(line): preserve status after body cleanup
* chore(changelog): avoid concurrent entry conflict
* style(line): format retry helper signature
* test(line): assert rejected media chunk
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(android): move device identity key into encrypted storage
Migrates the plaintext openclaw/identity/device.json Ed25519 signing key
into EncryptedSharedPreferences with a verified one-time import (synchronous
write, readback check, then plaintext delete) and no fallback reader.
Persistence failures now fail closed instead of silently continuing.
Documents why the cleartext NSC base-config must stay: NSC domain rules
cannot express user-selected RFC1918 gateway endpoints and OkHttp enforces
the policy, so GatewayHostSecurity remains the cleartext gate.
* fix(android): suppress UseKtx on synchronous identity commit
KTX edit(commit = true) discards commit's Boolean; the identity migration
fails closed on that result.
Model-call spans wrap outbound provider inference requests, so classify them as CLIENT independently of the optional GenAI naming convention. Generic tool spans remain unchanged because their shared events can represent local work.
Release note: operators filtering dashboards or alerts on span kind will see model-call spans move from INTERNAL to CLIENT.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The talk.speak synthesis path passed raw agent text to providers, so talk
mode could read fenced code, links, and tables aloud. Speech normalization
now happens once at the shared synthesis boundary (speech-mode stripMarkdown:
label-only links, decorative bullet/glyph cleanup, punctuation collapsing).
Code-heavy replies (>=50% fenced content, CommonMark-aware scanner incl.
blockquote/list containers and unclosed fences) get routed by surface:
talk.speak speaks a short on-screen fallback line, channel auto-TTS skips
the voice note since the visible text already carries the content, and
explicit conversions (tts.convert, tagged hidden TTS, chat Listen) always
speak stripped best-effort content.
* fix(plugins): roll back cleared runtime registrations when an activating reload aborts
loadOpenClawPlugins clears the process-global agent-harness, plugin-command,
compaction, detached-task, interactive-handler, embedding, memory-embedding,
and memory stores up front on an activating load and only re-registers them at
the end via activatePluginRegistry. The body has a finally with no catch, so a
throw during discovery or manifest load (a corrupted or half-written plugin
manifest mid-upgrade) leaves the previously active registry marked active while
its runtime stores are gone process-wide; the gateway reload driver only logs
and swallows the error.
Snapshot the process-global stores before the clear with the existing
snapshotPluginProcessGlobalState helper and restore them on any throw before
activation. clearActivatedPluginRuntimeState moves next to the snapshot and
restore helpers it pairs with in plugin-registration-transaction.ts, which
already owns atomic plugin registration state and imports from every module the
clear touches; loader.ts re-exports it so existing callers are unchanged.
* fix(plugins): make reload rollback activation-safe
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: distinct Responses tool-call ids for repeated native Kimi calls
The gateway tool (and any tool reused at the same call index) collapsed to
the same call_* id in OpenAI Responses replay because the id normalizer
hashed only the callId half of paired functions.<tool>:<index>|fc_tmp_*
ids, dropping the unique suffix. That broke replay with
invalid_replay_transcript: dangling_tool_call once a session called the
same tool at the same index more than once. Hash the full pairing instead.
* fix: resolve no-shadow lint error in openai.test.ts helper params
* test(agents): cover repeated Responses tool calls
* test(agents): type-check Responses id assertions
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(acp): scope /acp sessions listing for non-owner senders
/acp sessions listed every ACP session on the gateway for any sender
allowlisted via commands.allowFrom, exposing other senders' session
labels, agent ids, runtime state, and thread bindings. The handler now
returns only the current bound or requester session for non-owner
senders, while owner identity and operator.admin clients keep the full
gateway-wide listing, matching the documented contract.
Fixes#103055
* test(acp): cover empty and missing-session cases for /acp sessions scoping
* fix(acp): avoid non-owner session scans
* docs(acp): remove duplicated session scope text
* test(acp): cover internal session visibility
* fix(acp): require current ACP metadata
* test(acp): reject sessions target tokens
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(android): copy or save rendered widgets as images from the chat transcript
* chore(i18n): sync native app i18n inventory for widget export strings
* chore(i18n): regenerate native locale artifacts for widget export strings
* feat(ui): add composer microphone picker
* fix(ui): single check indicator in mic picker, matching house radio-menu pattern
* fix(ui): keep System default focusable during mic discovery; update composer contract test for picker caret
* fix(acp): persist cancelled partial replies
* fix(acp): persist delivered output for cancelled turns
Normalize terminal status at the ACP manager boundary and settle routed/direct delivery outcomes before persisting cancelled bound turns.
Co-authored-by: shaoohh <150606856+shaoohh@users.noreply.github.com>
* fix(acp): require confirmed cancelled-turn delivery
Persist cancelled-turn output only when the core dispatcher reports successful delivery. Keep canonical ACP history independent of outbound-only hook rewrites and prove backend cancellation with the real dispatcher.
Co-authored-by: shaoohh <150606856+shaoohh@users.noreply.github.com>
* test(acp): satisfy cancellation proof lint
Keep the pending-delivery race assertion behavior while avoiding a return value from the Promise executor.
Co-authored-by: shaoohh <150606856+shaoohh@users.noreply.github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(macos): add Quick Chat power features
* docs(macos): document Quick Chat power features
* chore(i18n): refresh native locale artifacts for Quick Chat power features
* fix(macos): update DEBUG test helper sendProvider to the reasoning-threaded arity