* feat(ui): unify sidebar footer into full-width identity card
Footer becomes one card (avatar + name + chevron) mirroring the top agent
card; identity menu gains email header, Settings (with platform shortcut
hint), and Usage; Usage leaves default sidebar pins; settings shortcut now
also binds Ctrl+Shift+Comma; offline state lives in the card subtitle and
menu retry action and the card persists while disconnected.
* test(ui): align persisted-sidebar fallback default with usage removal
Each legacy heartbeat tasks: entry becomes an editable system-created cron
job (declaration key heartbeat-task:<agentId>:<hash>, per-occurrence
identity for duplicate names) that fires a guarded heartbeat wake carrying
the task prompt plus current monitor scratch context. Task cadence is now
independent of the base heartbeat interval; active-hours, busy-retry,
min-spacing, and flood guards are preserved, deferred payload-carrying
wakes are retained and retried after the spacing floor, and colliding
task/event wakes cannot starve each other.
openclaw doctor --fix migrates existing blocks: async parse/plan, then one
synchronous SQLite transaction that rereads the pinned scratch revision,
upserts job rows, and strips the tasks: block atomically — concurrent
doctors serialize on the database and a losing run aborts untouched.
Orphan fields, invalid intervals, and incomplete entries block migration
of their block with a clear finding instead of silent text loss. The
runtime tasks: parser is deleted; leftover text is ordinary scratch prose.
Nine Codex gpt-5.6-sol xhigh autoreview cycles; final verdict clean.
* fix(cron): claim a same-generation session row instead of failing on benign concurrent writes
Under a large, busy session store, a concurrent write to a cron session
row that keeps the lifecycle revision unchanged (delivery/token/status
update) landing between session resolve and the run's first persist made
the claim guard reject the run with CronSessionLifecycleClaimError
"changed while starting work". canClaimInitialRevision required exact
ownership-field equality between the resolved snapshot and the freshest
row, so any drift aborted the run even though the unchanged lifecycle
revision proves no competing run had claimed it. The same guard shape
also aborts sessions_spawn subagent admission ("deleted while starting
work"). Store size amplifies the window: the full-store JSON
read-modify-write makes resolve->persist grow with entry count, so at
~22.5MB/576 entries every isolated run lost the claim continuously.
Allow the claim when the current row still carries the lifecycle revision
this run resolved from (same generation, no competing claim), still gated
by an inactive admission, and let the existing snapshot merge reconcile
the drifted fields. Genuine conflicts still fail hard: a different
revision claimed the row, or its revision is actively admitted.
Refs openclaw/openclaw#113085
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cron): keep session claims within one session id
Co-authored-by: Alex Markson <alex@metahacking.org>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(ui): merge creator avatar into sidebar leading slot and move creator filter into Threads menu
The per-row creator chip previously rendered next to the leading state
indicator, so rows with a known creator started their titles at a
different x-offset than rows without one. The avatar now occupies the
single fixed leading slot: unread renders as a corner badge on it,
running as a spinner ring, and open/merged PR state as a colored corner
badge, while attention and pinned icons keep the slot. Child rows keep
their status badges and no longer render owner chips.
The standalone 'Filter by creator' select above the session list is
gone; the Threads funnel menu gains a People radio section with owner
chips, and the funnel trigger shows an accent dot while a creator
filter is active. The empty Threads header stays visible when ownership
chrome is active so the filter can always be cleared. Mock dev fixtures
gain two creator identities so the ownership chrome is demonstrable.
* test(ui): guard parent row spread in child-avatar ownership case
The memory plugin's LanceDB path no longer regex-extracts or rewrites
[media attached:] marker text. Media-note lines are classified as
presentation-only and dropped wholesale during capture/query ingestion
(bracket-safe classifier: filenames may contain ']'); captions and
non-media text are preserved; legacy inline tokens stay inert escaped text.
Recall-side neutralization is deleted deliberately: image discovery is
scoped to the current-turn imagePrompt, so recalled memory text (markers
or bare path-shaped tokens) never reaches attachment scanning. Regressions
prove that boundary at the embedded, CLI, and plugin-harness entry points.
* fix(infra): use bigint stat in resolveSessionStorePathRelationship to prevent inode precision loss on large-inode filesystems
fs.statSync() returns ino as a JS Number, which can only safely represent
integers up to 2^53. On virtiofs / Kata Container filesystems (and any other
fs whose inodes exceed 2^53) two adjacent directory inodes — e.g.
72057594037932382 and 72057594037932383 — are both rounded to the same Number
value (72057594037932380), causing sameFileIdentity() to treat two distinct
paths as the same file object.
In resolveSessionStorePathRelationship() this manifests as a false-positive
alias detection: the agent state directory and its sessions/ child map to the
same inode Number, so the gateway migration refuses to continue and prints:
Deferred session key migration in aliased store …; atomic replacement cannot
update distinct filesystem aliases as one operation …
and the gateway refuses to start. The fix is a one-line change: pass
{ bigint: true } to fs.statSync() so that ino is returned as a BigInt that
preserves the full 64-bit value. FileIdentityStat already accepts bigint, and
sameFileIdentity already handles mixed number/bigint comparison, so no further
changes are needed.
Two regression tests are added to file-identity.test.ts documenting that
adjacent inodes above 2^53 are correctly distinguished when represented as
BigInt (the pre-fix Number path falsely collapses them).
Closes#112341
* test(infra): exercise bigint store identity
Co-authored-by: Rafli Surya Wijaya <260355617@qq.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor(heartbeat): make cron monitor rows own heartbeat cadence
agents.*.heartbeat.every stays the documented desired-state input; gateway
reconciliation and openclaw doctor --fix write it through into durable
system-owned cron monitor rows, and a scheduled monitor tick carries its
persisted everyMs/anchorMs as the authoritative cadence, which the runner
adopts for subsequent event cooldowns. Doctor gains a read-only cadence
preview (resolves the scheduler seed without creating device identity) and
an idempotent fix path that converges monitor rows per agent.
The cron-disabled fallback timer is deleted per its named removal plan
(#110950): gateways with cron.enabled=false or OPENCLAW_SKIP_CRON=1 log a
startup warning and get no scheduled heartbeats; manual and event-driven
wakes remain.
* fix(ci): drop unused requestHeartbeat import and refresh plugin SDK baseline
* refactor(media): delete internal MsgContext.Media* parallel fields for fact-only runtime
Internal runtime now carries a fact-only RuntimeMsgContext: the legacy
MediaPath/MediaUrl/MediaType parallel fields (and plural/transcribed
variants), their alignment and default helpers, and bundled legacy payload
builders are removed from internal paths. Staging, hooks, Gateway, media
understanding, and the Telegram, QQ, Signal, Slack, iMessage, Discord, QA,
and Zalo plugins consume ordered MediaFact[] directly.
Retained boundaries per the program audit: public Plugin SDK MsgContext,
persisted transcript Media* rows, and documented template variables keep
working via projectMediaFacts at the five declared seams (fact owner,
channel payload, inbound-event, transcript persistence, SDK adapter).
Review-round hardening, each with regressions:
- hasStagedMediaProjection requires every path-bearing fact staged (was
any-satisfied; mixed contexts skipped staging unstaged facts).
- finalizeInboundContext returns Omit<T, LegacyMediaContextKey> so deleted
legacy fields leave the type when they leave the object.
- QQ image facts carry explicit kind: "image" (remote URLs have no MIME).
- resolveStagedMediaFacts adopts staged legacy paths positionally while
retaining canonical fact metadata and cardinality (staged projections
previously replaced canonical facts wholesale).
1,615 tests across 47 files; goldens untouched; delegated check:changed
green (run 30009882062).
* test(media): migrate remaining legacy Media* consumer suites to structured facts
Exact-head CI caught suites outside the curated affected set that still
asserted internal legacy fields. All were stale internal-field assertions
migrated to facts, except QA Channel, which had a real migration gap: it
still constructed the bundled legacy payload internally and now normalizes
saved attachments with toInboundMediaFacts and passes the declared media
parameter directly. Exhaustive rg sweep over every test referencing the
deleted fields (133 files, each run individually) is green.