Honor canonical plugin identity across allowlists, denylists, and plugin mutations. Preserve and deterministically deep-merge alias configuration, and explicitly persist canonical entries in CLI enable and disable commands.
Closes#114320
Co-authored-by: kevin2966n <147227108+kevin2966n@users.noreply.github.com>
* feat(cron): bind agent turns to current sessions
* feat(commands): add loop chat command
* test(cron): cover session defaults and loop commands
* docs(cron): document current defaults and loops
* fix(commands): scope /loop status and stop by conversation name tag
* fix(commands): widen /loop conversation tag to 48 bits
* fix(commands): include disabled jobs in /loop status and stop
* docs(cron): regenerate docs map
* test(cron): split session-target default tests to satisfy max-lines
* fix(plugins): stop treating a partial active registry as authoritative for web providers
An active plugin registry with some web providers used to win even when a
manifest-declared candidate (e.g. an npm-installed Brave plugin with
BRAVE_API_KEY set) was absent from it, so env-var auto-detect could never see
installed external search providers. Delegate to the coverage-checked
resolvePluginWebProviders path, which reuses the active registry only when it
covers every declared candidate.
* feat(agents): require explicit overwrite before replacing pre-existing files in the write tool
Blind writes to an existing path silently destroyed user content (observed as
WildClawBench safety-task data loss with weaker models). The write tool now
refuses to replace an existing differing file unless the call passes
overwrite:true or this tool instance already wrote that path, keeping
iterate-loops friction-free while making destructive replacement an explicit
model decision.
* docs(templates): make a concrete first-message task outrank the BOOTSTRAP.md birth sequence
A fresh workspace's birth ritual hijacked substantive first messages: agents
introduced themselves and asked for a name instead of doing the requested
work (worst with weaker models, which follow the ritual literally). State
task precedence explicitly at the top of the template.
* fix(agents): lead the write overwrite guard error with the safe protocol
Weak models retried immediately with overwrite:true when the flag came first
in the message. Order the guidance read -> rename -> overwrite-as-last-resort
so the destructive path requires an explicit judgment call.
* refactor(agents): replace the write overwrite flag with a confirm-by-resend gate
The overwrite:true escape hatch let weak models bulldoze reflexively and grew
the tool schema. The first write to a differing pre-existing file now returns
that file's content (head-clipped) and a resend of the identical write, issued
after the warning, against byte-identical existing content confirms the
replacement. Fingerprints hash raw bytes; oversized files fall back to
size+mtime (named tradeoff); missing metadata fails closed.
* revert(agents): restore plain overwrite semantics in the write tool
The overwrite gate (flag, then confirm-by-resend) was overfit to one
WildClawBench safety rubric: the guarded model still chose to overwrite and
still scored zero, while every real overwrite in normal sessions paid a
round-trip. Write means write; peers (Pi, Hermes) agree.
* test(agents): drop the stale overwrite arg from the write output-contract test
* 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
* feat(ui): add a Memory settings page with Dreaming as a tab
Memory config was scattered across five surfaces: the memory.* schema section
lived on AI & Agents with 43 of 51 keys behind the Advanced tier, the memory
slot owner was only visible on Plugins, dreaming's knobs were JSON-only, its
status UI sat under Agents, and Memory Import was a separate route.
/settings/memory now owns that surface, following the MCP page shape (curated
rows above an embedded schema editor):
- Overview: the exclusive memory slot rendered as a segmented control over
installed memory-kind plugins, memory.backend promoted out of Advanced with
the qmd sub-config revealed only when qmd is selected, additive add-on rows,
and a Memory Import link.
- Search: the memory.search surface via the embedded editor.
- Dreaming: the global frequency/model/timezone/storage/phase knobs, which
previously required hand-editing openclaw.json, plus an agent picker feeding
the existing dream scene/diary/advanced panel for the agent-scoped reads.
Engine selection calls plugins.setEnabled so the gateway's exclusive slot
policy stays the single owner instead of being duplicated in the UI.
* fix(ui): redirect stale ai-agents memory deep links to the memory page
* fix(ui): report memory runtime defaults on the Memory page
The Dreaming tab rendered its own defaults instead of the ones
resolveMemoryDreamingConfig applies, so a config carrying only
dreaming.enabled showed all three phases off while they were running, and
an unset storage mode read as inline instead of separate. Toggle specs now
carry the runtime fallback and the storage default is stated once, both
pointing at src/memory-host-sdk/dreaming.ts.
Three more surfaces asserted things the runtime does not do:
- plugins.slots.memory "none" is the explicit-off sentinel, not an engine
id, so the segmented control selected nothing. The slot now resolves to a
closed auto/off/pinned selection with its own hint.
- memory.backend is resolved by the memory runtime the slot owner
registers, which only memory-core ships, so the row is hidden for any
other engine instead of saving a value nothing reads.
- The Dreaming tab wrote config.dreaming for whichever plugin owns the
slot even when that plugin's schema cannot hold it. It now reuses the
enablement flow's schema check (resolveDreamingConfigPathSupport, shared
with updateDreamingEnabled) and renders an unsupported state instead.
Also key the plugin-catalog sync on the connected phase: the connecting ->
connected transition keeps the same client object, so a page mounted
during the handshake never loaded the catalog and never showed the engine
picker.
The tab keeps the autosave status line and restart banner the embedded
editor renders on the other tabs; these knobs autosave, but nothing
reported it. The pure view moved to memory-dreaming.ts with the element in
memory-dreaming-page.ts, matching memory.ts/memory-page.ts.
* fix(ui): resolve the memory slot through the canonical policy
The Memory page re-derived plugins.slots.memory instead of using the rule the
runtime applies, which broke both directions of the engine control:
- An unset slot was reported as "the first enabled memory-kind plugin in the
catalog". The runtime resolves it to the slot's default owner
(DEFAULT_SLOT_BY_KEY.memory), so the page could show one engine as active
while another was loaded, reveal or hide the backend row for the wrong
plugin, and target the wrong plugin when switching memory off.
- Off called plugins.setEnabled(false), which writes enablement only. The slot
stayed pinned, so the choice did not survive a refresh and re-enabling that
plugin from the Plugins page silently switched memory back on.
resolveSlotSelection now lives next to defaultSlotIdForKey in
src/plugins/slots.ts and owns the rule once; config normalization consumes it
and the page imports it instead of restating it. Off writes the explicit "none"
sentinel through the config form, so it round-trips; picking an engine still
goes through plugins.setEnabled, which is where the exclusive slot policy
lives. The dreaming controller's own copy of the rule is gone too.
Four smaller fixes on the same surface:
- A failed engine change is reported next to the control instead of being
swallowed, so the selector no longer just snaps back.
- Dreaming's numeric inputs carry the memory-core manifest's integer/min/max
bounds and refuse out-of-range edits at the field, rather than patching a
value autosave then fails to write.
- Settings search destinations carry the Memory tab that renders the matched
child, so a memory.search hit no longer lands on Overview, whose narrowed
editor omits it.
- The Dreaming tab caches only a definitive schema-capability answer. An
offline or failed lookup now reports "unknown" and is retried on reconnect
instead of permanently suppressing the recheck.
* fix(ui): model unknown memory state instead of collapsing it
The Memory page reported unknowns as decided values. An empty catalog meant
loading, disconnected, or a failed plugins.list, yet add-on rows rendered
"Disabled"; catalog completions were keyed on client identity, which survives a
phase flip, so a stale load could repopulate a disconnected page or overwrite a
newer read; and `?tab=` was adopted once per distinct value, so a repeat
navigation to a tab the user had left was ignored.
Replace the ad-hoc nullable fields with closed shapes. MemoryCatalog is a
loading/unavailable/ready union, so absence of an entry only decides anything
inside `ready`, and MemoryAddonRow carries a four-state enablement the view
renders without ever inventing an "off". CatalogConnection is one object per
(client, connected) transition and doubles as the request generation an
in-flight load carries, so obsolete completions are dropped by identity. The tab
is no longer page state at all: the URL owns it, tab clicks navigate, and every
arrival is honored.
Settings search now resolves the engine/backend through the same
resolveMemoryBackend the page uses and matches only the `memory.*` children the
page can surface, so a `memory.qmd` hit under the built-in backend no longer
routes to an Overview whose editor omits it.
* fix(ui): surface a disabled memory owner and anchor curated backend search
The slot and plugin enablement are independent config surfaces, so
`plugins.slots.memory` can name a plugin the catalog reports as disabled.
The engine control showed that plugin as selected, and because re-picking an
already-selected radio fires no change event, there was no way back on. Add an
explicit enable row for that state and let the same-id write through when the
owner is not running; picking Off stays a no-op.
`memory.backend` is curated out of the schema editor, so the generic
`#config-section-memory` anchor scrolled past it. Fold the memory tab and hash
choice into one `memoryDestination` owner that routes a curated-only match to
the new anchor above the editor.
* fix(ui): scope the dreaming capability probe to its connection
The probe was deduplicated by plugin id alone, which cannot tell a current
answer from a stale one. A disconnect and reconnect on the same slot owner left
the token armed, so the reconnect read as "already in flight" and swallowed the
retry that an `unknown` answer requires — leaving an unsupported engine's knobs
editable until some unrelated config notification arrived. An A -> B -> A switch
had the mirror problem: the old A response was accepted for the new A probe.
Make the in-flight probe an object whose identity is the generation, drop it
whenever the owner or the connection changes, and accept only the completion
that still owns the slot. Same shape as the catalog guard on the Memory page.
* fix(ui): satisfy the lint and dead-export gates on the memory page
Exhaustive switches need a terminal `default:` to satisfy
typescript/consistent-return, matching the existing view-status.ts shape.
Seven symbols were exported with no production consumer outside their own
module, which the hard-zero Knip production scan rejects. Tests alone do not
make internals contracts, so drop the exports and reach the behavior through
each module's public surface instead: the view props type comes from
`Parameters<typeof renderMemory>`, the tab panel is found by its ARIA role, and
the dreaming number/storage helpers are proven through `renderDreamingSettings`.
Folding those helper unit tests into the render path also corrected one of them:
a `type="number"` input coerces unparseable text to empty, so the "reject
garbage" case was unreachable through the real control. Replaced with the
inclusive-bound and clear-the-field cases, which are reachable.
* refactor(ui): keep the memory schema facts out of the startup bundle
Settings pages are already lazy — the config route is `import("./config-page.ts")`
— but settings search runs from app-host at startup, and it needed the same
answers about which `memory.*` children are reachable and where a match lives.
Importing those from the view module dragged lit, hub-tabs, and settings-ui into
the startup chunk with it, blowing the Control UI startup budget.
Move the rendering-free facts (slot/backend resolution, tab and curated key
lists, schema narrowing, the anchor id) into memory-schema.ts, which imports
only record-coerce and the shared slot policy. The view keeps the templates and
now consumes the same module, so there is still one owner per fact.
* chore(ui): record the memory settings surface in the startup budget baseline
Routing settings search through memory-schema.ts instead of the view module
recovered 10,872 B of the startup chunk (334,992 -> 324,120 B), which is back
under the 324,608 B ceiling. The remaining 2,795 B over the old baseline is the
honest cost of the new surface: its i18n strings, plus the slot/backend facts
the startup search index has to read.
Measured by hosted CI (run 30189972795); this worktree cannot build locally
because pnpm wants to purge a node_modules shared with other running agents.
* fix(plugins): import adopted catalog history into the OpenClaw transcript
Import native Pi and OpenCode history when a catalog session is adopted so the OpenClaw transcript reflects the full conversation immediately.
Mark imported user rows with mirrorOrigin because they are transcript mirrors, not new external turns. This preserves watcher deduplication and prevents adopted history from being reported as fresh upstream input.
* refactor(plugins): split catalog history importer
Keep the session catalog contract module leaf-like by moving transcript mutation into a dedicated runtime module. This breaks the registry-to-transcript import cycle while preserving the Plugin SDK entrypoint and importer behavior.
* 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
* chore(compat): date the annotated deprecation families and expose removal-pending debt
Adds 10 dated compat-registry records for shipped deprecated surfaces that
previously had @deprecated annotations only (removeAfter 2026-10-01 per the
approved 60-day window), extends the beta5 session-store record with the
package-root aliases, surfaces removal-pending records with due dates,
blockers, and readers in the plugin boundary report, and converts undated
cleanup obligations (CI timing job, zizmor excessive-permissions, staged
Codex CI auth, memory-host legacy state dir, agent-DB schema-ladder floors)
into dated markers visible to the weekly dated-TODO sweep. Metadata,
diagnostics, and docs only - no runtime behavior change.
* fix(compat): keep extension src test paths out of core string literals
The core import guardrail forbids extensions/*/src/ path shapes in core
production string literals; cite the package-root google-meet test and the
registry test for official-plugin-export-aliases instead.
* feat(plugins): support additional upstream session kinds
* feat(acpx): monitor upstream Pi sessions
* feat(opencode): monitor upstream OpenCode sessions
* fix(opencode): ignore hidden user text activity
* refactor(plugins): share upstream echo filtering
* fix(opencode): preserve marker tuple narrowing
* refactor(opencode): detect upstream turns via event_sequence cursor
Replace timestamp/grace-window completeness heuristics with OpenCode's own
per-session event_sequence.seq cursor, which advances inside the same
transaction as the message/part projections. Detects change instead of
inferring completion, so staged part writes can no longer drop a turn.
Handles cursor regression from OpenCode migrations that clear event_sequence,
suppresses compaction replay and summary re-publish, and restricts human-turn
classification to visible non-synthetic text.
* docs(plugins): record why the adoption coordinator requires complete
* fix(opencode): align cursor proof with strict types
* fix(acpx): keep Pi JSON parser private
* test: consolidate OpenClaw test state fixtures
* test(plugin-sdk): expose isolated test state
Promote the isolated OpenClaw test-state lifecycle through a narrow published Plugin SDK subpath so extension tests no longer import private core helpers. This intentional SDK surface addition is maintainer-approved.
* test: use SDK test-state seam in extensions
Route bundled extension suites through the focused repo-local Plugin SDK test-state entrypoint and remove the Codex projector harness exports made stale by fixture consolidation. Keep the seam out of production builds and published package artifacts while auditing its real consumers in the full-tree deadcode scan.
* test(plugins): map test-state in package boundaries
* feat(models): add Claude Opus 5 support
* test(models): align Opus 5 setup fixtures
* test(models): update ambient Opus 5 detection
* fix(models): reconcile Opus 5 support with main
* feat(anthropic): add Claude Opus 5 model support
Wire claude-opus-5 through the Claude 5 contract seams: adaptive-by-default
thinking with the full low..max effort range, default-sampling and prefill
stripping, streaming refusal contract, model-bound thinking replay, and 1M/128k
catalog metadata across anthropic, claude-cli, Vertex, Bedrock, and Mantle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(anthropic-vertex): normalize explicit Vertex Opus 5 model rows
Extend normalizeAnthropicVertexResolvedModel so user-configured Vertex Opus 5
rows regain reasoning, image input, 1M/128k limits, and the native thinking
map; update the live-model priority expectation and regenerate docs_map.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 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