* feat(ui): live agent activity subtitles on running sidebar sessions
* refactor(ui): lazy-load narration controller off the startup chunk, unexport test-only symbols
* chore(ui): raise startup JS budget to 312 KiB for the lazy narration feature
* test(ui): restore real timers after narration controller cases
* feat(browser): send pages to the main session from the Chrome extension
One-click page share in the OpenClaw Chrome extension: toolbar popup with an
optional note, page/selection context menu, and Alt+Shift+S. Capture is
selection-first with a readability heuristic, X/Twitter thread extraction, and
Google Docs plain-text export via the user's session cookies. Payloads ride the
existing paired relay WebSocket as a new pageShare message; the gateway-only
page-share sink wraps page text in the external-content safety boundary, then
enqueues a main-session system event and requests an immediate heartbeat
(hooks/wake semantics). Node-hosted relays report a clear unsupported error.
Capture heuristics adapted from Nat Eliason's MIT-licensed send-to-openclaw.
Co-authored-by: Codex <codex@openai.com>
* fix(browser): keep page-controlled metadata inside the share safety boundary
Review findings: move title/URL inside wrapExternalContent (a hostile <title>
must not become trusted header text), prefer the user's selection over the
full Google Docs export, and pass the context-menu selectionText through so
iframe selections and selections cleared during relay reconnect still win.
* fix(browser): bind context-menu shares to the click-time document
Selection shares from the context menu now send the click snapshot directly
(no recapture), so navigations during relay reconnect cannot mislabel the
source and iframe selections are preserved. The Google Docs selection probe
scans all accessible frames before falling back to the full-document export.
* test(browser): expect the page-share handler in relay server args
* fix(browser): probe only the main frame for Google Docs selections
All-frame injection rejects wholesale when one frame is inaccessible and
returns child frames in nondeterministic order, so the probe now reads the
main frame only. Child-frame selections still share correctly through the
context menu's click-time selectionText; toolbar/shortcut entry sends the
full page for that case (named tradeoff in the code comment).
* fix(browser): satisfy page-share CI gates
---------
Co-authored-by: Codex <codex@openai.com>
Live-append voice transcripts into the agent session and persist a per-agent SQLite call record across relay and client transcript paths.
Add run-scoped spoken confirmation for high-impact actions, mutation digests, bootstrap-context injection, talk.client.transcript and talk.client.close protocol methods, and Control UI adoption. This adds zero new configuration.
Co-authored-by: Clifton King <clifton@users.noreply.github.com>
* feat(codex): fork upstream-linked sessions at a message via thread/fork
* fix(gateway): fail closed for rewind and branch switch on upstream-linked sessions
* fix(codex): fail closed on first-message forks, image-only prompts, and orphan archival
* fix(codex): baseline retained history and reject paginated threads on upstream fork
* fix(codex): validate the full fork prefix and fail closed across crash windows
* fix(codex): treat all non-text inputs as unverifiable in fork drift checks
* fix(codex): support first-message forks as empty-history upstream cuts
* fix(codex): reject source-id reuse and unverifiable hidden inputs in fork boundaries
* refactor(codex): materialize upstream forks from verified thread read-back
* fix(codex): satisfy strict type lanes and knip for upstream fork
* fix(agents): preserve ANSI sanitizer state across bash chunks
* fix(agents): harden streaming ANSI sanitization
Keep incremental parser state in the canonical terminal owner, avoid a second sanitizer pass, and leave OutputAccumulator and public terminal APIs unchanged.\n\nCo-authored-by: Jicheng Xu <xu.jincheng@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(sessions): stop persisting media-only caption placeholder into transcripts
Media-only user turns persisted the literal "[User sent media without
caption]" as message content, so every client rendered the model-facing
scaffolding as a visible subtitle under the image. Persist bare content
instead and inject the placeholder at the single LLM-boundary stamping
site (like per-message timestamps, #3658), keeping provider-visible
bytes identical for current turns, history replay, and legacy
transcripts.
* fix(agents): satisfy lint and test-type gates for boundary media injection
* feat(gateway): auto-approve trusted-proxy browser device pairing
Adds gateway.auth.trustedProxy.deviceAutoApprove so team gateways behind an
identity-aware proxy (Cloudflare Access, oauth2-proxy, Pomerium) can skip the
manual `openclaw devices approve` step for new Control UI/WebChat devices.
Auto-approval fires only for a new (unpaired) operator browser device on a
connection that already passed trusted-proxy auth with a resolved allowUsers
user. Scope upgrades on existing devices and node pairing stay manual. Granted
scopes are capped to the configured set intersected with the connection's
x-openclaw-scopes proxy cap, operator.admin is rejected at config validation,
and the pairing-store approval rechecks new-device status under the store lock
so a repair/upgrade or concurrent approval can never be silently widened. Each
auto-approval emits an audit log line with the proxy user and granted scopes,
and `openclaw security audit` warns when the mode is enabled.
* docs: regenerate docs map for trusted-proxy auto-approval section
* feat(config): journal every config change with source labels and manual-edit detection
* fix(config): narrow journal exports and align plugin-write test expectations
* refactor(config): split journal snapshot store out of io.audit
* chore(config): keep journal snapshot context helpers module-local
* fix(status): carry prepared thinking catalog
Resolve status thinking levels from model metadata prepared by each caller.
Model-state paths carry their resolved catalog forward, while standalone status
surfaces make one side-effect-free read without provider discovery.
Co-authored-by: Bartok9 <danielrpike9@gmail.com>
* test(status): assert read-only native catalog load
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cli): bound exec approvals --file JSON read size
Replace raw fs.readFile with the shared readRegularFile helper
from @openclaw/fs-safe/advanced, which enforces regular-file
validation and a max-bytes limit. The --stdin path already had
a 1 MB bound via readStdin; --file now uses the same
EXEC_APPROVALS_STDIN_MAX_BYTES limit.
* test(cli): add --file read bounds regression tests
Covers normal (under limit), oversized (> 1 MiB), and non-regular
path (directory) --file inputs to the approvals set command.
* fix: import readRegularFile from ../infra/fs-safe.js for boundary compliance
* fix(cli): preserve approvals file path behavior
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* 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