* feat(sdk): always persist media facts and ship facts-first replacements for legacy Media* surfaces
PR 1 of the media legacy retirement program (audit-frozen, 4 PRs).
- Every media-bearing user turn now persists normalized __openclaw.media
facts unconditionally while continuing to emit the legacy top-level
Media* projection byte-identically (dual-write bridge; the conditional
shouldPersistStructuredMediaEntries gate now always includes media).
- New replacement APIs, shipped before any removal: typed hook media
facts (media[], originalMedia[], mediaStagingPending) on message
events; {{AttachmentPath}}/{{AttachmentUrl}}/{{AttachmentContentType}}/
{{AttachmentDir}}/{{AttachmentIndex}} template variables; focused
openclaw/plugin-sdk/media-local-roots subpath split out of the
deprecated agent-media-payload facade.
- Every legacy surface carries @deprecated naming its replacement, under
one named compatibility record media-legacy-projection with the
operator-approved removeAfter 2026-10-01 (two release trains; deletion
additionally gates on a clean published-plugin artifact sweep).
- Generic transcript append invariant documented; SDK migration, hooks,
and configuration docs updated to the facts-first path.
Writer golden matrix proves legacy bytes and model prompt bytes are
unchanged while nested facts become unconditional. 2,189 broad media
tests green; SDK api-baseline regenerated on fresh-env Testbox.
* feat(sdk): register media-local-roots subpath exports and deprecation metadata
Completes PR 1: package export map for openclaw/plugin-sdk/media-local-roots
plus the deprecated-subpath inventory and doc metadata entries for the
media-legacy-projection record.
* chore(sdk): track media-local-roots entrypoint and deprecated-export budgets
* fix(sdk): keep deprecated MSTeams buildMediaPayload re-export through the compat window
Deleting shipped runtime-api re-exports belongs to retirement PR 4 after
the media-legacy-projection window; PR 1 only deprecates. Also formats
the migration-guide schedule table.
* docs: regenerate docs map for media migration additions
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.
* 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
* 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(channels): add channel-owned setup contracts
* test(channels): align legacy setup fixtures
* chore(channels): regenerate config and SDK baselines after rebase
* fix(update): run fresh doctor after current-process core changes
* fix(channels): align add pre-scan with execution precedence
* style(cli): format channels-cli test additions
* fix(channels): restore option-before-positional channel resolution via metadata arity scan
* fix(channels): keep help flags out of metadata arity escalation
* test(update): mock fresh post-update doctor in current-process suites
* style: format review fixes and correct entrypoint mock type
* fix(channels): register only modern contract options for dual-publishing plugins
* test(update): align downgrade suites with fresh-doctor child invocation
* docs(channels): record empty-contract and input-forwarding invariants
* fix(line): keep the shipped --token switch as a channel access token alias
* fix(signal): stop treating exact cross-family loopback endpoints as bind-aligned
* chore(config): regenerate docs config baselines after second rebase
* style: format rebased channels add tests
* fix(channels): enforce field-key and flag-name agreement in setup contracts
* fix(signal): detect container endpoints for bare --http-url setup
* fix(signal): ignore unconfigured accounts in transport collision checks
* fix(channels): validate negated setup flags in contract and normalizer
* fix(signal): preserve existing transport kind when setup detection is unreachable
* style(signal): use direct boolean check in collision guard
* style(signal): type test config literals
* docs(update): record two-read design of fresh-doctor validation gate
* fix(channels): satisfy post-rebase architecture gates
* docs: refresh channel setup map
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
A Gateway timeout or closed connection now fails the command with an
actionable stderr hint instead of silently re-running the whole turn
embedded under a fresh gateway-fallback-* session. The silent fallback
could double-execute side effects (the Gateway may still finish an
accepted turn), returned context-free answers to --session-key callers,
and ran with the CLI host's local config. --local remains the only
embedded execution path.
Also deletes the resultMetaOverrides plumbing (the fallback was its only
writer) and the fallback marker fields added in #111645.
Final sweep of the media-placeholder program: with all eleven producers
migrated, the consumer regexes are dead input and are hard-deleted —
media-understanding token un-mixing (bodies are pure user text now),
the persisted-user-turn placeholder drop, the session-memory strip, and
the inbound-event mediaPlaceholder field with its replacement branch
(an intentional removal of shipped SDK surface per the program's
hard-delete decision: plugins still minting placeholder bodies render
them as literal text until updated). formatMediaPlaceholderText remains
the single owner of the <media:kind> vocabulary, used only for
text-only carriers. Legacy tokens in pre-migration history remain
ordinary text.
* perf(state): cap the per-agent SQLite handle cache with LRU eviction
Multi-tenant hosts open one WAL database per agent (~3 file descriptors
each); the process-local handle cache was unbounded, so large fleets
exhausted descriptors. Cache hits now refresh LRU recency, and cache-miss
opens evict the oldest non-transactional handle before constructing the
new one, capped at 64 open handles. Eviction closes the process-local
handle only; registry rows and durable data are untouched and evicted
databases reopen transparently on next access.
* fix(state): fleet-wide introspection reads stop opening writable agent databases
Live testing an 81-agent gateway surfaced three full-fleet sweeps that
opened every agent's SQLite database writable (schema ensure + registry
write transaction per open): the 60s health snapshot's per-agent session
summaries, usage-cost cache reads behind the gateway usage endpoints, and
the zalouser doctor detector that ran from every CLI startup. Each sweep
churned the new bounded handle cache and saturated the event loop.
Session listing gains a readonly non-registering variant (SDK exposes it
as an additive readOnly flag on listSessionEntries); health and usage
reads use it, with transient-lock tolerance owned by the health caller.
The zalouser detector gates on the channel/credentials actually existing,
collects once per pass, and detection reads go readonly while migration
apply stays writable. The usage refresh queue's flat 50ms busy retry
becomes exponential backoff capped at 5s, and refreshing summaries retain
their cache timestamp so the 30s TTL gates fleet rescans.
* perf(state): make per-agent handle eviction and reopen cheap
Eviction only works if reopen is cheap. Reopens after eviction previously
repeated full first-open work; ensureOpenClawAgentSchema takes BEGIN
IMMEDIATE and the registry upsert writes the shared state DB, so reopens
blocked in synchronous busy waits (observed 14-22s event-loop stalls with
reconcile workers holding write transactions). Owner/schema validation and
registration now run once per path per process with invalidation on
quarantine and disposal; the read-only integrity guard still runs on every
physical open. Eviction closes with a PASSIVE WAL checkpoint instead of
TRUNCATE, which waits on readers; orderly dispose keeps TRUNCATE so
sidecars are flushed before unlink.
* ci: regenerate plugin-SDK API baseline, drop prod test-seam exports, mock readonly accessor
The additive readOnly flag on the SDK listSessionEntries changed the
declaration surface, so the generated API baseline is regenerated via the
sanctioned script. Knip forbids production exports consumed only by tests:
the usage-cache testApi export is removed outright and the refresh-queue
seam moves behind an env-gated global exposed by a test-support module.
health.plugins.test's full session-accessor mock gains the newly imported
readonly listing.
* feat(channels): batch 1 producers drop media placeholder bodies
Media-placeholder program batch 1: Google Chat, Zalo, LINE, and
Mattermost stop minting <media:kind> placeholder bodies. Media-only
messages carry an empty caption plus one structured fact per native
attachment (type-only when a download fails or is rejected, so payload
positions and kind signals stay aligned). The shared
formatMediaPlaceholderText SDK formatter renders text-only carriers
(Mattermost pending-room lines) from structured facts; per-channel
placeholder builders and the expected-count side channel are deleted.
* fix(mattermost): satisfy type, deadcode, and SDK manifest gates
* feat: add session discussion panel seam
* fix: keep discussion iframe cookie-capable and changelog release-owned
* test: cover cookie-capable discussion iframe sandbox
* fix: stretch discussion panel host so the embed fills the rail
* fix: keep provider failures retryable and probe discussion availability before showing the action
* fix: block same-origin discussion embeds, show action on catalog sessions, close stale panel on reconnect
* fix: scope discussion probes and panel callbacks to the issuing connection
* fix: dedupe in-flight discussion probes per session
* fix: retry superseded discussion probes and key-scope panel results
* fix: regenerate Swift protocol models and extend advertised-method expectations
* style: format chat-pane-header
* fix: regenerate Kotlin protocol models and date discussion methods in the 2026.7 train
* chore: restore release-owned changelog to main state
* chore: keep changelog untouched relative to merge-base