* 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.
* build(deps): remove npm shrinkwrap; mirror pnpm lock into transient package locks
npm 12 removed shrinkwrap (command + tarball/root loading). Delete all 82
committed npm-shrinkwrap.json files and stop publishing lockfiles; keep
pnpm-lock.yaml as the single reviewed dependency boundary. The generator
becomes scripts/generate-npm-package-lock.mjs and feeds plugin bundling via
a transient package-lock.json + npm ci (works on npm 11 and 12). Tarball
validation treats the published 2026.7.2 beta train as a shrinkwrap
transition; self-update npm detection now uses install topology instead of
the shipped shrinkwrap.
* fix(deps): repair lint, deadcode, and test-type lanes for the npm 12 migration
- sort integrity comparisons with an explicit comparator (oxlint)
- keep resolveBunGlobalNodeModules module-local (knip unused-export gate)
- model npm pack --json as npm<=11 array / npm 12 name-keyed object
- default calver destructuring in the tarball test fixture
* 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(cron): system-owned heartbeat monitor jobs replace the interval scheduler
- new internal cron payload kind {kind:"heartbeat"}: execution pokes
requestHeartbeat({source:"interval"}); reported in the protocol job
schema, not accepted from client create/patch
- gateway converges one declaration-keyed monitor job per heartbeat-enabled
agent (schedule every+deterministic phase anchor) at startup and on
config reload; removes monitors for unconfigured agents
- heartbeat runner loses its interval setTimeout machinery; nextDueMs
stays as the cooldown gate, event wakes unchanged
* test(cron): heartbeat monitor regressions; docs for cron-owned cadence
- converge/prune/failure-containment tests for heartbeat monitor jobs
- heartbeat payload run fires an interval wake, no system event
- scheduler tests converted from timer self-fire to wake-queue pokes;
timer-mechanics-only tests deleted with the timer
- persisted-shape accepts the heartbeat payload kind
- docs: heartbeat cadence ownership + system payload kind
* fix(cron): heartbeat monitor review round 1
- targeted cron-monitor interval ticks use the full per-agent path so
due-commitment sessions still deliver
- cron-disabled gateways keep a local fallback interval timer (shipped
cron.enabled=false contract; removed when heartbeat config folds into
cron in #110950)
- heartbeat job reconciliations serialize with latest-wins epochs and a
bounded 30s retry after a failed convergence pass
* fix(cron): chain clamped fallback heartbeat timers past the setTimeout cap
* fix(cron): heartbeat monitor review round 3
- targeted monitor redirect skips wakes carrying heartbeat overrides and
surfaces the per-agent terminal skip reason instead of not-due
- cron-disabled fallback timer re-arms with a 1s floor after each firing
so a dropped wake cannot end the chain
- heartbeat payloads are system-owned at the service boundary: add requires
the gateway opt-in, patches to the kind are rejected
* fix(cron): heartbeat monitor review round 4 — full ownership enforcement
- prune only jobs proven to be monitors (prefix AND heartbeat payload)
- existing monitors reject every update patch; declarative upserts on the
monitor key require the gateway opt-in even with a different payload
* fix(cron): complete heartbeat monitor ownership boundary
- converge scopes declarative matching to real monitors so a colliding
user job with the same key is never adopted or overwritten
- monitor removal requires the gateway systemOwned opt-in; ad-hoc
API/CLI deletion is rejected, reconciliation cleanup still prunes
* docs(cron): record intentional enrollment-snapshot semantics for monitor ticks
* fix(cron): repair heartbeat monitor CI gates
The root package.json shipped "author": "", so npm showed no author for the
published openclaw package, and the three published workspace packages had no
author field at all. Set all four to the Foundation, matching the LICENSE
copyright holder and the openclaw.ai footer.
Also name the Foundation on the docs homepage, which never said who develops
OpenClaw even though the README and website footer now do.
Personal credits in docs/reference/credits.md, docs/start/lore.md, and the site
footer stay as authorship history; they are not org attribution.
* feat(ui): redesign session observer HUD with labeled status and visibility-gated digests
* chore(protocol): regenerate app protocol bindings for sessions.observer.visibility
* 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
* 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
* fix(ui): allow direct sessions in non-Git folders
* test: align New Session fixtures with place picker
* fix(ui): preserve worktree intent on Git probe failures
* fix(types): keep worktree status alias internal
* feat(ui): session observer HUD, subtitle integration, and settings
* test(ui): observer demo fixtures for the mock control-ui harness
* fix(ui): satisfy lint and deadcode gates for observer surfaces
* test(ui): adopt renamed pull-request summary api after rebase
* fix(ui): clean rebase artifacts in observer test files
* feat(cron): stream schedule sources (supervised command stdout)
Add gated argv stream schedules with bounded line batching and trigger.streamBatch composition.
Reuse the gateway ProcessSupervisor for source ownership, deterministic teardown, capped restart backoff, and schedule-key guarded batch execution. Expose additive protocol, CLI, tool, UI, docs, and generated snapshot surfaces without storage DDL.
Contract: stream schedules are event-driven, require cron.triggers.enabled, reject command payloads, and retain at most one bounded pending batch.
* fix(cron): reject retired stream source epochs at run admission
Thread an invalidatable per-owner source-generation token (ownerNonce.generation)
through cron.run admission alongside the schedule key. A batch handed to cron.run
under one source epoch can wait behind another run while its owner is stopped; a
disable→re-enable or A→B→A edit leaves the schedule key unchanged, so the key
check alone would admit the retired epoch's batch. The token is persisted in
job.state on every lifecycle write and compared at every admission site plus the
executeJobCore guard, so a stale epoch's batch is skipped.
Also fix direct stream-job mutations recording the wrong lifecycle status when
global cron is off but triggers are on: extract resolveStreamStopReason so the
direct path reports the remediable cron-disabled state like reconcile does.
* fix(cron): close stream admission windows from round-10 review
- Persist the retired source generation before draining stop teardown, so a
batch queued behind another cron run cannot gain admission during the up-to-10s
in-flight-batch wait (server-cron routed stop path).
- Add streamSourceGeneration to the closed gateway response schema (excluded from
the writable patch schema) so a running stream job passes strict result
validation without letting callers spoof source identity.
- Close the mutation-epoch ABA: track an eviction epoch so a snapshotted absent-0
is trusted as unchanged only when no LRU eviction happened during the await.
* refactor(cron): stream sources own a durable logical identity
Split the conflated restart-generation/admission token into two concepts:
a persisted streamSourceIdentity owned by cron store mutations (rotates on
enable/disable, source replacement, once-trigger auto-disable, and explicit
retirement; stable across supervised child restarts) and a watcher-local
process generation used only to fence stale child callbacks. Admission now
requires schedule key + identity together at every window, closing the
A-to-B-to-A and restart-flush races from review rounds 8-11.
Also: match-mode regexes now see raw source text (the [truncated] marker is
applied after matching), stop-timeout failures set the live restartExhausted
mirror so shutdown preserves the terminal diagnostic, and the watcher is
split into owner/output/registry modules under the max-lines budget.
* fix(cron): harden stream teardown and intake from round-4 review
An exit queued ahead of a requested stop no longer counts toward restart
exhaustion (the synchronous stop fence owns it), overlapping cron.stop and
stopAndDrain share one memoized shutdown drain instead of double-stopping
every owner, raw output intake is bounded at 4x the batch cap so normal
64 KiB pipe reads stop losing complete lines to OS chunk boundaries, and
persisted-shape quarantine coverage for unsafe match expressions is pinned.
* fix(cron): keep watcher-internal owner disposal from retiring live identity
Disposing an obsolete owner while a start replaces it is not a durable
removal; a retiring stop there rotated the live job's identity and stranded
the replacement behind the CAS ownership guard. Also align the docs with the
implemented match semantics: complete lines match on full text past the
batch cap, only intake-cut prefixes are unmatchable.
* fix(cron): make oversized-line matching independent of pipe chunking
Partial lines are retained up to the raw-intake bound rather than the
delivery cap, so a complete over-cap line matches identically whether it
arrives in one callback or several; only a line the intake bound itself cut
remains an unprovable prefix. Reconcile also contains schedule-replacement
stop failures per job, matching the other stop branches.
* fix(cron): bound assembled lines, drop stale payload override, barrier stopAll
- enforce the 4x raw-intake per-line cap while assembling split callbacks,
so an oversized line stays an unprovable prefix regardless of chunking
- stop passing the watcher-cached payload as a cron.run override; the run
snapshots the persisted payload under its admission lock
- stopAll waits for every owner stop to settle before surfacing failures
- split cron-stream-output interleaving tests into their own file (max-lines)
* fix(cron): address first full-CI round (lint, knip, schema test, unused param)
* chore(cron): refresh codex prompt snapshots for stream schedule schema
* fix(cron): keep the first clean line after an intake drop ending at a newline
* fix(cron): fence stream reconcile list snapshots against direct mutation routes
A cron.list snapshot captured across the reconcile await could be applied
after a direct add/update route already started the owner, stopping it as
removed and retiring its live identity. A mutation revision bumped at every
direct route start invalidates the stale snapshot; reconcile re-lists
(bounded) instead of applying it.
* style(cron): format stream owner imports
* fix(cron): discard severed stream prefixes at EOF
* fix(cron): retry failed stream shutdown drains
* fix(cron): honor stream stop fence after output drain
* chore(cron): refresh landing checks
Branch menu (list/switch) for macOS+iOS shared chat via sessions.branches.*;
rewind/fork/switch gated on run activity and pending outbox work through a
durable session-mutation lease; outbox rows carry branch-epoch ownership with
local-only flush checks, atomic confirm/park transitions, attempt-versioned
delivery callbacks, fresh retry identity for possibly-accepted rows, and
epoch-guarded branch evidence; 21-locale native translations for new strings.
* feat(gateway): session observer digests over the utility model
* fix(gateway): split session-observer modules and satisfy ci gates
* fix(gateway): observer reads session entries without materializing agent state