* fix(ui): keep stable chat rows in insertion order and only sort live tool/stream items by timestamp
* fix(ui): keep live rows within current turn
* fix(ui): keep current work above queued turns
* fix(ui): keep streamed replies above queued turns
* fix(ui): preserve reconnecting run order
* fix(ui): preserve causal terminal ordering
* fix(ui): bound replay rows to owning turns
* fix(ui): keep question summaries in owning turns
* fix(ui): scope question run ownership to session
* fix(ui): restore reconnecting chat run identity
* fix(ui): remove unused chatItemTimestamp import in chat-thread-build.ts
* fix(ui): correlate question summaries with agent runs
Co-authored-by: Peter Lee <li.xialong@xydigit.com>
* chore(i18n): refresh native source baseline
Co-authored-by: Peter Lee <li.xialong@xydigit.com>
* fix(ui): remove unused chatItemTimestamp export and split tool-stream test file
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(ui): durable session board face and dashboards index
Board face lived only in client-side boardSessionViews, capped at 50 entries,
so the preference never followed the user to another device, evicted as
sessions accumulated, and could not be seen as a set.
Persist it as SessionEntry.boardFace, which rides the existing entry_json blob
and so needs no SQLite schema change or version bump. Expose it on the session
list row and add it to the sessions.patch write-scope allowlist alongside label,
pinned, and archived: setting your own view preference is user-level chat
organization, not policy. Unknown patch fields still fail closed to
operator.admin.
Generic navigation now reads the stored face, so the sidebar and session list
open a thread on the face you left it on. boardSessionViews keeps only
activeTabId and reopenDockByTab, which are genuinely per-device.
Add /dashboards listing threads whose preferred face is dashboard. Filtering
runs server-side in filterSessionEntries before pagination, because the client
holds only a capped page and a client-side filter would silently omit
dashboards.
* test(protocol): assert the pre-rename face param is rejected
The gateway-protocol validator test still passed the pre-rename 'face' key,
which the closed schema rejects. Use boardFace, and pin the old name as a
negative case so it cannot silently return.
* chore(protocol): regenerate Swift bindings and docs map for boardFace
Adding boardFace to the sessions schema changes two committed generated
artifacts: the Swift gateway models (pnpm protocol:gen:swift) and the docs map
(pnpm docs:map:gen), which now lists the dashboards index section.
* refactor(prompt): plain inbound context labels with a provenance marker
Replaces trust-worded inbound context labels ("(untrusted metadata)",
"(untrusted, for context)") with plain labels plus a fixed provenance
marker suffix appended to every OpenClaw-injected context header.
Detection keys on the marker, not label text, so strippers stay correct
across UI, TUI, replay, /trace segmentation, memory recall, and the Swift
chat preprocessor. Drops sanitizeInboundSystemTags in favor of the marker
boundary plus trusted system-prompt narration.
Renames the untrusted-named plugin SDK context identifiers to
channel-provenance names, keeping deprecated aliases registered for
removal after 2026-09-08.
Adds `openclaw doctor --fix` migrations that rewrite legacy inbound
labels in stored SQLite transcripts and purge legacy envelope-
contaminated LanceDB recall rows.
* fix(ci): resolve gate failures for plain inbound context labels
- doctor sqlite readers: open read-only connections via openNodeSqliteDatabase
so the Kysely connection-boundary guardrail holds; unexport the now-internal
transcript snapshot type (Knip unused-export gate).
- compat registry: split the record table into registry-records.ts and
plugin-sdk-subpath-records.ts. The new compat record pushed registry.ts past
the 700-line oxlint cap; suppressions are disallowed, so follow the existing
sibling record-module pattern. Public exports and PluginCompatCode literals
unchanged.
- acp-runtime test: assert current finalization behavior (newline normalization
only). The bracket de-fang and System: rewrite it expected were removed with
sanitizeInboundSystemTags; forged system lines are neutralized at the
system-event queue, the single chokepoint feeding the System:-per-line render.
- regenerate docs_map and the plugin SDK API baseline manifest.
* fix(prompt): harden inbound context label migration and drop in-band sanitizer
Review follow-ups on the plain-label + provenance-marker change:
- Remove src/security/system-tags.ts. Rewriting inbound text to neutralize
look-alike `System:`/`[System]` markers corrupted legitimate user text and is
not a real injection boundary; role separation plus external-content wrapping
is. Explicit product decision, recorded at the system-event queue.
- Narrow the LanceDB legacy-row purge so it cannot delete benign memories. It
now requires a complete known legacy sentinel line, a legacy label followed by
a fenced JSON body, or the complete legacy external-content header. The prior
predicates matched ordinary prose such as `Notes (untrusted metadata):`, and
deletion is irreversible.
- Make explicit-empty canonical ChannelStructuredContext win over the deprecated
alias via a present/absent result instead of collapsing `[]` to undefined.
- Keep `\r?` in the active-memory doctor rule. It is the only rule spanning the
header's line break, migrated assistant rows skip newline normalization, and
without it the marked-header replace wins and the body strips to empty. Added
a CRLF regression test.
- Fix stale comments that described removed behavior, and cover the Swift
prose-block strip path.
Claude-Session: https://claude.ai/code/session_01WNzsPddQmxy9Y7jKD4wAxH
* refactor(infra): move exec approvals into the shared SQLite state DB
Delete the file-runtime exec-approvals store (exec-approvals.json + .lock
sidecar machinery) on both runtimes and make the reserved
exec_approvals_config singleton row canonical. Doctor owns the one-time
import with claim/verify/receipt discipline; runtime fails closed with a
doctor instruction while un-migrated legacy state exists. The wire CAS
contract, socket semantics, and gateway auth-token derivations are
unchanged. Kills the #113929 lock-contention bug class structurally and
nets around -2.9k lines.
* fix(infra): green CI gates and retire file-era exec approvals tests
Break the migration-type import cycle with a leaf contract, regenerate the
plugin-SDK API and native i18n baselines for the intentional surface change,
drop unused exports, and replace the macOS file-era approvals test suite with
SQLite-backed behavior coverage per the obsolete-internals test policy.
* chore: green max-lines ratchet, native i18n baseline, and unused-export scan
* feat(ui): gateway-owned sidebar section order with hover-only drag grips
* fix(ui): repair sidebar section order CI gates
* fix(protocol): keep sectionOrder optional for older gateways
Native Swift clients ship separately from gateways, so new clients must decode older gateway responses that omit sectionOrder.
* fix(ui): reconcile gateway section ordering with main
* fix(state): allow lazy tables in v5 maintenance
* refactor(ui): retire prefs session section order in favor of gateway-owned order
Supersedes #113948 by deleting the unreleased ui.prefs.sessionSectionOrder key and its browser plumbing. Gateway SQLite sidebar_sections remains the single canonical store.
* refactor(gateway): remove dead sessions.observer.ask rpc
* docs: record btw and companion contract split
* fix(gateway): unexport observer model sanitizer after ask removal
* feat(gateway): add web-only incognito sessions held in process memory
* feat(ui): add incognito toggle and badges to the web new-session flow
* fix(sessions): classify incognito by key shape, fail closed on stale keys, and gate memory writes
* fix(codex): start harness threads ephemeral for incognito sessions
* fix(sessions): reshape internal-effects incognito keys and add doctor repair for reserved key collisions
* refactor(plugin-sdk): export canonical incognito key classifier and guard the sentinel path
* fix(state): classify incognito DB handles from the recorded open-time set
* fix(gateway): isolate incognito sessions from durable lineage and allocation on read-only misses
* docs(sessions): pin the reserved incognito namespace ownership decision
* feat(gateway): admin-scope incognito visibility and incognito-blind cross-session surfaces
* fix(ci): repair kysely guardrails, dead export, docs map, protocol bindings, and ACP reset rotation
* fix(gateway): remove non-admin observability side channels for incognito sessions
* fix(gateway): enforce admin-scope incognito access and cover all parent-reference creation paths
* feat(sessions): enforce visibility and membership
* feat(ui): add session sharing controls
* docs: add session sharing implementation report
* refactor(sessions): use canonical creator identity
* fix(sessions): adopt creator ownership contract
* docs: refresh session sharing rebase report
* docs: record final creator integration proof
* docs: record final main rebase
* chore: drop worktree report artifact
* fix(sessions): keep drafts owner-only
* fix(ui): preserve redacted session restrictions
* fix(sessions): preserve scoped sharing authorization
* fix(sessions): re-verify session instance inside sharing mutation queue
* test(sessions): cover stale sharing mutation
* fix(sessions): bind membership to session instance, gate absence blocking on sharing
* fix(sessions): preserve entry normalization on rebase
* fix(sessions): atomic visibility instance guard, reset visibility on recreate
* docs(ui): name the absence-heuristic tradeoff and link follow-up
* feat(protocol): expose session sharing row state
* docs: note generated creator identity type
* fix(sessions): bind member writes and visibility rollback to session instance
* fix(ui): discard stale-connection sharing loads; drop worktree scratch files
* fix(ui): block composer only on observed sharing state, never on list absence
* fix(gateway): authorize agent runs against the resolved session (close keyless bypass)
* chore(protocol): allowlist Control UI-only session.sharing event for mobile
* test(config): record session.sharing keys in common-tier snapshot
* refactor(sessions): unexport internal sharing helpers
* test(gateway): update sessions changed routing assertions
* fix(sessions): align sharing identity with created actor
* fix(sessions): align membership identities and storage keys
* fix(gateway): re-filter drafts against fresh sharing state in sessions.list
* fix(gateway): drafts stay owner+admin only in the sessions.list fresh filter
* fix(ui): re-export sharing protocol types for the Control UI
* fix(ui): keep SessionSharingRole internal to satisfy deadcode gate
* fix(gateway): read runtime config lazily in session-mutation authorization
authorizeSessionMutation ran on every gateway request but eagerly called
context.getRuntimeConfig() — a non-trivial config resolve — for methods that
are never session mutations. Read config only once a real session-mutation
target is resolved. Also register the four session sharing methods in the
2026.7 release-train inventory test.
* fix(gateway): share one config snapshot across session-mutation authorization
Group rename/delete discovery and the authorization loop were each resolving
runtime config separately after the lazy-read change. Memoize the resolve so
non-session requests still pay nothing, while any session mutation resolves
config at most once and both discovery and authorization use that single
snapshot (no double reload, no mid-request config-change split).
* fix(gateway): resolve session-sharing CI gates
- isGatewayAdmin: null-safe connect access so internal/plugin-runtime runs
(which reach authorization with a connect-less client) do not crash.
- emitSessionsChanged: scope only to a concrete session key; a [undefined]
sessionKeys scope filters nothing correctly and would strip draft gating.
- session stores: mark the sync TOCTOU re-read and the sqlite_master
existence probe as narrowly-justified raw SQLite primitives.
- tests: provide getRuntimeConfig to the session-action contract context,
drop a shadowed 'call' binding, use structuredClone, and assert the
agent-scoped sessions.changed broadcast shape.
* docs(gateway): note best-effort participation gate + refresh native i18n baseline
Session ownership/visibility are usability features, not a security boundary
(docs/concepts/multi-user.md, SECURITY.md); document that the pre-dispatch
authorization is intentionally not commit-bound to the resolved instance.
Also refresh apps/.i18n/native-source.json after the session-sharing protocol
codegen shifted line numbers of existing native strings (position-only).
* test(gateway): reset session sharing snapshots
* style(gateway): format sharing reset import
* fix(ui): disable the thread-changes button when the workspace is not a git checkout
* chore(protocol): regenerate Swift gateway models for sessions.files.list gitCheckout
* feat(ui): redesign session observer HUD with labeled status and visibility-gated digests
* chore(protocol): regenerate app protocol bindings for sessions.observer.visibility
* feat(agents): mobile_ui agent tool for Android UI control (PR 3/3)
Add a dedicated model-facing tool that drives another Android app through the
PR2 mobile.ui.observe/act node commands, completing the vertical slice
(agent -> tool -> node.invoke -> AccessibilityService).
- Mirrors the desktop computer tool's safety model: owner-only + HTTP-denied
(dangerous-tools), raw node.invoke of mobile.ui.* redirected to this tool so
the generic nodes tool cannot bypass it, run/tool-call idempotency, and the
phone-arm workflow (mobile.ui.* must be explicitly armed).
- One call is observe or one act; every act automatically re-observes for
postcondition verification and preserves the landed outcome if re-observe fails.
- Fail-closed confirmation: all state-changing acts (activate, set_text, tap,
swipe) require confirmed=true after the model reviews the proposed effect;
observe, scroll, wait, and navigation global_actions do not. The keyword list
only enriches the confirmation message and is never the sole gate (a11y labels
are localized/iconographic/coordinate-blind).
- Node selection resolves an explicit id against the full device set first
(case-insensitively) and rejects an ineligible or ambiguous match, so an
explicit selection can never be silently redirected to the wrong phone.
- All observed UI text is treated as untrusted; the tool description forbids
following instructions found in app UI.
Additive; no protocol bump. mobile-ui-tool + registration + policy tests pass;
core tsgo and lint verified on Testbox. On-device drive is exercised via the
PR1/PR2 emulator proof; this PR is the agent-side tool + tests.
Follow-up: computer-tool.ts has the same latent node-resolution ordering bug
(explicit id searched only among eligible nodes); tracked separately.
* fix(agents,phone-control): migrate mobile_ui arming hints/tests to gateway.nodes.commands.allow
Match main's node-command arming rename (gateway.nodes.allowCommands ->
gateway.nodes.commands.allow / commands.deny): update the mobile_ui tool arm-hint
matchers to the gateway's current rejection strings and fix the phone-control /
tool tests to the current config shape. Production write path was already correct.
* fix(agents): register mobile_ui in owner-only denylist; trim phone-control under max-lines
- tool-resolution.exclude.test.ts: mobile_ui joins the owner-only core
tools, so add it to the expected plugin/inherited denylists.
- phone-control: derive ArmGroup from a single ARM_GROUPS const and
collapse parseGroup's ||-chain, dropping index.ts back under 700 lines.
* fix(agents): centralize mobile_ui global-action names; regenerate tool-display snapshot
- Extract GLOBAL_ACTION_NAMES const so the schema, action type, and
validator share one source. This also removes the bare `name: "back"`
type-annotation literal that scripts/tool-display.ts's name-regex was
misreading as a phantom runtime tool.
- Regenerate apps/.../tool-display.json to add the mobile_ui display entry
(was missing from the Swift snapshot).
* feat(sessions): first-class archived-session handling
Archived sessions are now exempt from every automatic maintenance path
(age prune, entry cap, model-run prune, disk budget, cleanup repairs);
only explicit sessions.delete removes them. sessions.list gains an
additive tri-state archived filter (true | false | "all"). The sidebar
gets a persisted Active/Archived/All status filter with inline dimmed
archived rows, replacing the nav-away View-archived button; the Sessions
page toggle becomes the same tri-state with dimmed+badged rows in All
mode; the chat composer's archived notice becomes a full-width banner
with an Unarchive action.
* chore(protocol): regenerate Swift gateway models for tri-state archived filter
* feat(apple): add rare working claw stances
Add zen, drummer, and peekaboo animations to the shared iOS and macOS working indicator, with deterministic stance and pose coverage.
* chore(i18n): refresh native source anchors
* refactor(gateway): declare spawn lineage explicitly at sessions.create
Spawn depth is now a declared fact, never inferred from parentSessionKey.
sessions.create accepts an optional spawnDepth (requires parentSessionKey) that
spawn-owned creations pass; every other fresh session persists spawnDepth 0,
making operator chats and UI forks spawn-capable roots. The visible spawn tool
declares callerDepth + 1, and subagent depth recovery no longer walks
parentSessionKey, which is UI threading only.
Accepted tradeoff (documented inline): pre-upgrade visible children whose only
lineage was parentSessionKey resolve as roots; the transient population may
spawn one extra generation, still capped by maxChildrenPerAgent.
* chore(protocol): regenerate Swift gateway models for sessions.create spawnDepth
* feat(gateway,ui): ask-the-observer card input over sessions.observer.ask
* refactor(ui): single home for observer run-identity helper after restack
* test(ui): drop duplicated observer hud test after restack
* test(ui): give the observer ask flow its own colocated suite
* refactor(gateway): leaf observer contract and ask module split for ci gates
* refactor(gateway): drop observer contract re-export shims