Slack appends task_update details/output per update for the same id (only title/status replace); the native progress stream re-sent every row each snapshot, so bash rows accumulated "completedcompleted…". Reconcile now emits only changed rows with append-only field deltas, keeps status words out of details, and puts the once-emitted result (file delta or failing exit) in output.
Preserve attempt-local recovery state so successful mutation retries emit a redacted terminal receipt without stale failure warnings.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(node-host): recover MCP catalogs and sessions
Share placement-neutral MCP lifecycle and result projection while keeping Gateway session ownership and node process ownership separate. Refresh node catalogs live, recover closed or expired transports without replay, and preserve MCP application errors across node.invoke.\n\nFixes #125044
* test: register node MCP CI inventory
* fix(node-host): stop MCP recovery after abort
* fix-android-chat-session-picker
* fix-search-all-android-sessions
* fix-native-i18n-inventory
* fix-android-share-session-browser-policy
* fix(android): show loading during session search
* refactor(android): move gateway-backed session search into the pinned sidebar
The sidebar header and search field no longer scroll away: they sit above
the scrolling sections, and the search field is always visible instead of
hidden behind a toggle. Queries now run through the shared session-browser
search state (debounced gateway search with offline fallback) instead of a
local filter over cached rows, and matching threads replace the section
list while a query is active.
The in-chat bottom-sheet picker is removed: the compact switcher's All
button navigates straight to the Sessions screen again, and the sidebar
owns in-context session search. The shared rememberSessionBrowserSearchState
extraction from the Sessions screen is kept and gains the sidebar as its
second consumer.
* style(android): fix sidebar import ordering for ktlint
---------
Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(update): keep a committed post-core result when stopping the child signals
The post-core wait settles from three places: a result-file poll, child "error",
and child "exit". The poll stopped the child before claiming the settle, and
stopping delivers a signal, so the still-unclaimed exit handler could reject with
"post-update process terminated by signal SIGTERM" for an update whose child had
already written a valid result. The outer catch then restored the tentative
plugin index, rolling back work that actually succeeded.
Claim the settle first, then stop. The exit handler sees settled and returns.
Net zero production lines; pure reordering plus the invariant comment.
This is the error that leaked across tests in the agentic-cli shard on
2026-08-17 (run 31997770370), where it surfaced inside an unrelated test and
cascaded into 34 failures in src/cli/update-cli.test.ts.
Regression test drives the exact ordering: the child commits its result, then the
parent's stop delivers SIGTERM. It fails on pre-fix code with
"promise rejected ... instead of resolving / post-update process terminated by
signal SIGTERM".
* test(update): satisfy strict indexed access in the post-core signal regression
* feat(cron): enable automation triggers by default
Condition watchers, script payloads, and stream schedules were gated behind
cron.triggers.enabled=false, so the capability shipped dark and the automations
tool told the model to call it unsupported.
The gate did not buy what its warning implied. cronJobUsesToolRuntime treats
agentTurn, script, and trigger.script identically for tool policy, and jobs are
capped to the creating agent's allowlist via creatorToolAllowlist -- but an
agentTurn cron job was never gated, so unattended recurring exec with that same
creator-capped authority was already reachable. The gated paths are strictly
tighter: a condition gate gets 30s, 5 tool calls, 16KB state and a 30s minimum
interval, where an agentTurn has no such budget. Sandboxing already applies to
trigger scripts, which resolve sandbox context and redirect the workspace when
access is not rw.
Absent config now means enabled; an explicit cron.triggers.enabled: false still
disables every surface it disabled before, and the error text names the opt-out
instead of telling operators to turn something on. No new config key, no
migration.
Docs: reword the trigger warning for default-on while keeping the unattended
execution note, and record the new default in the configuration reference.
* test(cron): refresh prompt snapshots for default-on triggers
Trigger, stream-schedule, and script-payload surfaces are now advertised in the
automations tool description by default, so the committed fixtures drift.
This also records the cost: dynamicToolsJson grows 49,477 -> 52,541 chars and
the snapshot total 76,861 -> 79,925 (~766 rough tokens per prompt). That is the
price of no longer dark-shipping the capability.
* test(mcp): expect trigger surfaces by default in the tools bridge
The MCP tools bridge mirrors the scheduler gate, so an absent cron.triggers
config now advertises the trigger surface. Explicit false still narrows it and
explicit true still widens it; both assertions are unchanged.
The deleted case took ~54s and exceeded the 120s per-test timeout under CPU contention. Its coverage is already provided by the sibling pre-split SDK bridge case, which spans both private-local-only and public alias classes, and by extensions/llama-cpp/index.test.ts.
* fix(sessions): flatten Markdown in session list previews
Session-list previews were extracted verbatim from the last transcript
message, so raw Markdown leaked into every surface that renders the
subtitle as plain text — Control UI sidebar, TUI picker, native session
lists, and the sessions_list tool. A finished session read as
"Landed [PR #124879](https://github.com/...)".
Flatten lastMessagePreview at the Gateway producer, inside the
watermark-validated title-field cache, so the cost is amortized and no
consumer re-implements stripping. The flattener is the regex chain that
already existed privately in the Control UI narration line; it moves to
@openclaw/normalization-core/markdown-plain-text and both surfaces now
share one implementation. Session titles keep their own normalization
and are unchanged.
Also let an unread final observer digest outrank the raw last reply in
the sidebar subtitle, so the utility model's headline wins the slot it
was written for. The finalDigestUnread gate is untouched, so an
already-read digest still falls back to the flattened preview.
* fix(ci): register Markdown preview module
* fix(sessions): preserve literal preview punctuation
* fix(sessions): flatten previews before truncation
* fix(test): adapt session projection callback
* fix(security): warn on main-scoped group rooms
* fix(security): ignore direct-only group scope bindings
* fix(security): honor group binding precedence
Every surface that survives only until the messages.visibleReplies default
flips to message_tool now carries a greppable TRANSITIONAL(marker-retirement)
comment naming that trigger: the reply/audio parser family, streaming
tail-buffering, the write-boundary applier, automatic-mode prompt teaching,
the [[tts]] DSL + streaming cleaner, and the live narration strip. Comment-only.
GitHub-hosted images carry Node 24.19.0 in /opt/hostedtoolcache, so hosted jobs
resolved from there and never populated the cached root. They still ran a
restore that could only miss and then a save whose path did not exist, logging
"Path Validation Error: Path(s) specified in the action for caching do(es) not
exist" on every hosted job. No entry was ever written, so nothing was poisoned,
but the warning is noise and the steps are pure waste on ~30 jobs per run.
Gate both steps on runner.environment != 'github-hosted', the signal this
workflow already uses for Blacksmith-only behavior, and gate the save on the
setup step reporting that a download actually populated the root. That second
guard also covers a future self-hosted image whose toolcache clears the floor.
Verified on Blacksmith: a satisfying image toolcache leaves the root absent so
the save is skipped, and a download populates it so the save runs.
* fix(config): materialize fresh-install defaults for missing config
A missing config file (fresh install) skipped runtime-defaults
materialization on both loadConfig and readConfigFileSnapshot, while an
existing empty {} config got the full defaults (compaction safeguard,
session/cron/model defaults). Out-of-box behavior silently diverged from
the documented defaults until the first config write created the file.
The drift dates to bc75968074, which dropped the missing-branch
materializeRuntimeConfig call during an import-trim.
Route the missing-file branches through the same load/snapshot
materialization as existing configs, and delete the now-unreferenced
per-mode defaults profile table in materialize.ts — load and snapshot
must materialize identically anyway (prepared-runtime exact-config
resolution depends on it), so the profile indirection only invited
drift.
* chore(ratchet): shrink io.load.ts assertion baseline to 2
The missing-config fix removed an 'as OpenClawConfig' cast; the
assertion-safety ratchet requires the baseline to shrink with it.
* refactor(gateway,ui): one bounded display projection; delete marker strip sites
Persisted transcripts are marker-free since the write-boundary projection
(#124793), the historical migration (#124888), and TTS facts (#124913), so
display surfaces stop compensating. sessions.list.lastMessagePreview and its
siblings (sessions.preview/describe, TUI picker, sessions_list tool, MCP) now
share one bounded role-aware projection (240 chars, tool/system/thinking and
suppressed control replies excluded, directive-only rows fall through). The
web reply chip reads the typed openclawDelivery fact instead of parsing text;
chat.history preserves the field to the UI. Post-hoc display strips are
deleted across web/TUI/MCP/sessions-list; live streaming cleaners stay.
Stale gateway-protocol preview comments corrected; no schema change.
Assertion-safety baseline pruned for shrunk files (sanctioned direction).
Production net -173, tests net -137. Fixes the sidebar [[reply_to_current]]
preview leak and the empty-code-pill overstrip of quoted markers.
* fix(agents): preserve restart recovery transcript reads
* refactor(gateway): remove obsolete transcript exports
* fix(gateway): normalize injected delivery directives
* fix(ci): scope projection and recovery checks
* chore(ci): shrink plugin SDK surface budgets
* test: deflake loaded side question and worker checks
* test: align display projection CI fixtures
* style: format display projection fixture
* fix(gateway): bound startup history materialization
Apply SQLite byte limits before loading history payloads and share a yielded, fingerprinted Claude CLI snapshot across concurrent startup clients. Preserve marker, cursor, redaction, and external identity semantics, and clarify that shrink-only ratchet updates need no separate approval.
* test(browser): await all lazy command groups
* fix(gateway): bound history snapshot state
* fix(gateway): preserve oversized history responsiveness