* fix(doctor): gate cross-state-dir legacy imports behind explicit doctor opt-in
A gateway or CLI command started with OPENCLAW_STATE_DIR pointing at a
non-default directory used to import legacy files FROM the default
~/.openclaw state dir (exec-approvals.json, plugin-binding-approvals.json)
and archive the originals with a .migrated suffix. Any isolated, test, or
staging run on a host with production state silently captured and archived
the production files.
Cross-state-dir imports now run only with the new crossStateDirImports
opt-in: openclaw doctor --fix (or an interactive doctor confirm) performs
the import; the implicit CLI/gateway preflight leaves the default dir
untouched and emits a notice pointing at doctor instead.
* test(doctor): include notices in legacy-state detector mocks
* fix(agents): recover claude-cli warm-stdin continuity when no native transcript is written
The headless warm-stdin claude-cli backend (liveSession: "claude-stdio")
never writes a native transcript, so the post-turn flush probe always
fails and the missing-transcript reuse path drops the bound session id.
Part 1 (cli-runner.ts): scope the non-destructive binding behavior to
warm-stdin sessions so they keep their binding instead of clearing it
every turn.
Part 2 (attempt-execution.ts): on a missing transcript, clear the stored
binding (no stale --resume) but still return the bound id as the reuse
candidate so prepare can re-detect the missing transcript and arm
raw-transcript reseed. Returning undefined starved reseed and lost
warm-stdin continuity.
Adds/updates regression coverage in attempt-execution.cli.test.ts and a
complementary reseed test in prepare.test.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(agents): align warm-session continuity coverage
* test(agents): keep cli live-session mock complete
* fix(agents): respect stateless CLI session mode
* style(agents): keep session candidate guard focused
* test(agents): preserve minimal CLI runner fixtures
* fix(agents): preserve exact Claude warm sessions
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* perf(gateway): replace O(n^2) char loop with regex in stripDisallowedChatControlChars
Replace character-by-character iteration with a single regex replace to
avoid event-loop blocking on large messages (1MB+). The regex matches
only disallowed control characters (NUL-BS, VT, FF, SO-US, DEL) while
preserving tab, newline, CR, printable ASCII, and Unicode.
Closes#102915
* perf(gateway): use String.fromCodePoint for regex to avoid lint suppression
Replace the eslint-disable comment and hex-escape regex literal with
String.fromCodePoint() construction. This avoids adding a new entry to
the lint suppression baseline while keeping the same character set and
performance characteristics.
Ref: #102915
* refactor(gateway): consolidate chat sanitizer
---------
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(proxy-capture): truncate inline preview on UTF-8 character boundary
persistEventPayload stores a small UTF-8 preview (dataText) inline for fast CLI
listings and query output, capped at previewLimit bytes (default 8192). It built
the preview with buffer.subarray(0, previewLimit).toString("utf8"), which slices
the buffer at an arbitrary byte offset. For any non-ASCII body (emoji/CJK/accented
text, common in model-provider HTTP traffic), the cut lands mid-multibyte-sequence
and Node emits a trailing U+FFFD replacement char into the stored preview column.
Decode the full buffer first, then truncate with the existing truncateUtf8Prefix
helper (src/utils/utf8-truncate.ts), which backtracks off UTF-8 continuation bytes
so the preview ends on a complete character while staying within the byte budget.
ASCII previews are byte-identical; only malformed trailing U+FFFD is removed.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(proxy-capture): align stub store return type with SharedCaptureBlobRecord
The preview-boundary test stub returned a bare { blobId, sha256, byteLength },
which fails tsgo typecheck against persistPayload's declared
CaptureBlobRecord | SharedCaptureBlobRecord return. Return a complete
SharedCaptureBlobRecord instead. Also use template strings for the multibyte
fixtures to satisfy no-useless-concat. No assertion behavior change.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(proxy-capture): byte-bounded UTF-8 preview, unify store and proxy paths
The prior fix decoded the whole payload with buffer.toString("utf8") before
truncating, an availability regression for large captured payloads, and it left
the proxy-server finishBodyPreviewCapture path on the same byte-subarray decode
that can split a trailing multibyte sequence into U+FFFD.
Add truncateUtf8PrefixFromBuffer: decodes only the byte-bounded prefix and walks
back to verify the trailing multibyte sequence is complete (handles both a hard
byte cap and a buffer already sliced mid-sequence upstream, e.g. a dangling lead
byte). Use it from persistEventPayload (no full-payload decode) and
finishBodyPreviewCapture (same boundary safety), so both preview paths share one
canonical helper.
Add completeUtf8PrefixLength coverage plus proxy-server body-preview regression
tests for dangling-lead, byte-cap, emoji, and oversized-body cases.
Co-Authored-By: Claude <noreply@anthropic.com>
* style(proxy-capture): fix no-useless-assignment and unnecessary template expr
- utf8-truncate.ts: expected is reassigned in every branch, so declare without
the unused initial value (no-useless-assignment).
- proxy-server.body-preview.test.ts: drop the redundant template wrapper around
a single expression (no-unnecessary-template-expression). No behavior change.
Co-Authored-By: Claude <noreply@anthropic.com>
* docs(utf8-truncate): clarify completeUtf8PrefixLength byte-budget contract
Document that maxBytes is a byte (not character) limit and that the returned
length is the largest offset that does not split a multibyte sequence.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(proxy-capture): preserve UTF-8 preview boundaries
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(copilot): redact OAuth error response body in fetchJson error messages
Replace raw response body text with bounded, redacted structured error detail
extracted via extractProviderErrorDetail so OAuth error responses containing
tokens, device codes, or other sensitive fields are not leaked into Error
messages and downstream logs.
* test(copilot): add device-code and non-JSON error body redaction cases
Add regression tests covering the device code flow and non-JSON error
bodies alongside the existing token refresh coverage. Also include live
proof output showing real Response object redaction via
extractProviderErrorDetail.
Ref: #102953
* fix(copilot): normalize OAuth HTTP failures
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(outbound): restore current-conversation binding map when persist fails
The generic current-conversation binding mutators (bind/touch/unbind/
prune-on-read) changed the process-wide in-memory map before calling
writePersistedBindings(), with no rollback. A throwing SQLite write left
the map ahead of disk; because bindingsLoaded is a one-time flag, the
diverged bindings were served until restart, routing inbound messages to
wrong or deleted session targets while the caller already saw the throw.
Add persistBindingsOrRestore(): snapshot the map before mutation and, on
write failure, restore it and rethrow. Covers all six write sites. Mirrors
the cron rollback precedent (#99960, src/cron/service/ops.ts persistOrRestore).
Fault-injection tests assert the map reverts for each path.
* fix(outbound): publish conversation bindings after SQLite commit
* chore(outbound): align binding files with current main
* fix(outbound): publish conversation bindings after SQLite commit
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Preserve the cloned task snapshot and deterministic activity ordering while avoiding the createdAt sort that tasks.list immediately replaces.
Closes#101716
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cron): preserve cron context in session entry for async completion wakes (#99919)
Persist bootstrapContextRunKind on the session entry after cron agent
runs, and restore provider/model/thinking/runKind from the session
entry into directAgentParams when an async completion wake (e.g. media
generation) resumes a cron session.
Before this fix, async completion wakes lost the original cron run
context and fell back to account defaults, causing multi-step cron tasks
to silently dead-end after the first async media generation call.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cron): authorize model override on trusted in-process dispatch for cron context restoration
* fix(cron): persist bootstrapContextRunKind on base session entry for async completion wakes
* fix(cron): route async completion wake to base cron session key instead of ephemeral run key
When the cron run-key fallback fires, record the base cron key as
effectiveSessionKey so deliverDirectSubagentAnnounce can route the
agent call to the persisted session row with its transcript and task
context instead of starting a fresh turn (#99919).
* fix(cron): add bootstrapContextRunKind to session entry slot keys and declare loadRequesterSessionEntry return type
* fix(cron): rebase onto upstream main, restore upstream agent-command behavior while keeping bootstrapContextRunKind persistence
* fix(cron): persist bootstrapContextRunKind from a pre-mutation snapshot to produce a real delta
* fix(cron): move pre-mutation snapshot inside guard so initialEntry is non-optional
* fix(cron): preserve async media continuation context
Co-authored-by: Peter Lee <li.xialong@xydigit.com>
* fix(cron): retain continuation after base cleanup
* test(gateway): complete agent scope mock
* test(gateway): preserve agent scope exports
* test(cron): persist mocked session mutations
* test(cron): model persisted session fixtures
* fix(agents): snapshot cron continuation retries
* fix(cron): harden async continuation settlement
* test(cron): control continuation recovery clock
* fix(gateway): narrow continuation model row
* chore(changelog): defer cron continuation note
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
A fun field guide for the Control UI lobster: what it is, when it
visits, how to interact (hover names, pokes, right-click shoo), the
Appearance toggle, the Lobsterdex, reduced-motion and privacy notes.
Deliberately teases rather than documents the rare variants, events,
and the anniversary. Registered in the Web interfaces nav group and the
generated docs map.
Arrivals (visits and offline summons) log their palette id into a
best-effort localStorage set. Settings -> Appearance gains a Lobsterdex
row: mini canonical lobsters for all twelve palettes, seen ones in
color (grails keep their glow/translucency via the palette classes),
unseen ones as dim silhouettes, with a seen/total count. Minis render
standalone (closed-lid layer inline-hidden) since they live outside the
pet's CSS context. New quickSettings.appearance strings synced across
locale bundles.