Two crashes when a memory plugin misbehaves in bridge mode:
- listActiveMemoryPublicArtifacts sorted plugin-returned artifacts
without
validating them; an artifact missing any of the string fields the
comparator dereferences (kind, workspaceDir, relativePath,
absolutePath,
contentType) crashed wiki status and every other bridge consumer with
"Cannot read properties of undefined (reading 'localeCompare')".
@mem0/openclaw-mem0 <= 1.0.14 shipped record-shaped artifacts with
none
of those fields, typed against a drifted SDK stub. Validate the shape,
drop malformed entries (and non-array listings), and warn once naming
the offending plugin -- the same treatment agentIds already got.
- resolveMemoryWikiStatus gated artifact counting on vaultMode/enabled
but
not bridge.readMemoryArtifacts, so the wiki.status gateway method
still
enumerated artifacts (and hit the crash above) with the flag off, even
though the sync path (bridge.ts) and CLI gateway routing honor it. The
documented workaround therefore never worked for the wiki_status agent
tool. Add the flag to the gate; the count reports null when imports
are
disabled, matching non-bridge modes.
Gateway (additive, no protocol version bump): SessionEntry gains
lastReadAt/markedUnreadAt/lastActivityAt; session rows expose a derived
unread flag (explicit mark, or last read before latest activity; never-read
sessions stay read so upgrades do not light up). lastActivityAt is stamped
in the canonical post-run store update - user, channel, and cron runs count
as activity; heartbeat, internal-event, and preserved-state runs do not.
sessions.patch gains unread; sessions.create gains fork (transcript fork
from parentSessionKey under the parent lifecycle lock, refusing active,
concurrently-changed, and oversized parents, cross-agent aware).
Web sidebar: Pinned/custom-group/Ungrouped sections, unread dots, kebab and
right-click context menu (pin, mark unread/read, rename, fork, move to group,
archive, delete guarded for agent main sessions and active runs), mark-read
on view with loop-safe re-acknowledgement and failure retry; sessions page
gets unread + fork actions and shared custom-group helpers.
iOS Command Center: grouped sections, unread/pin indicators, Show Archived
gated on per-entry state, full context menu with rename/new-group alerts and
delete confirmation, current-session preview guarantee, read-episode
re-acknowledgement; new patch/delete/fork transport calls; Swift protocol
models regenerated.
Android SessionsScreen: grouped headers, unread/pin indicators, Archived
filter gated on per-entry state, long-press menu with the full control set,
agent-scoped forks, explicit label/category clears from session events,
main-session fallback when archiving/deleting the open chat, read-episode
re-acknowledgement with failure retry.
Closes#100739
* fix(plugins): discover config load.paths plugins when index has entries
Installing a managed npm plugin populates the persisted plugin index.
Once the index has entries (index.plugins.length > 0),
loadPluginManifestRegistryForInstalledIndex is used instead of the
full discovery path. This path only processes plugins that already
exist in the index — if workspace plugins from config plugins.load.paths
are missing from the index, they are silently dropped from the manifest
registry, producing 'plugin not found (stale config entry ignored)'
warnings on gateway startup and in CLI commands.
Fix: after converting index records to plugin candidates, also
discover plugins from config plugins.load.paths and merge any
candidates that are not already represented in the index-based
list. This ensures config-origin workspace plugins are always
included in the manifest registry regardless of index completeness.
Fixes#99185
* fix(plugins): address clawsweeper review — config-only scope, pluginId scoping, regression tests
- P1: Limit extra discovery scope to config-origin candidates only
(filter by origin === 'config') instead of full discoverOpenClawPlugins
- P2: Preserve pluginId scoping for load-path candidates
(filter extra candidates by pluginIdSet when present)
- Add 3 regression tests: load.paths discovery, empty-load no-op,
pluginId scoping preservation
- Add L2 real behavior proof with direct function call evidence
🦞 diamond lobster: L2 evidence (real function call + objects)
Ref. https://github.com/openclaw/openclaw/pull/99196
* fix(plugins): replace discoverOpenClawPlugins with config-only discoverFromConfigPaths
P1: The installed-index fallback previously called discoverOpenClawPlugins,
which scans bundled/global/workspace roots and returns their diagnostics.
This allowed unrelated shared-root diagnostics to leak into the
installed-index config validation surface.
Replace it with discoverFromConfigPaths — a new helper that scans only
the user-configured plugins.load.paths entries via discoverFromPath
directly, producing zero bundled/global candidates or diagnostics.
P2: pluginId scoping is preserved for load-path candidates.
Added discoverFromConfigPaths to src/plugins/discovery.ts to keep the
config-only discovery path reusable and explicit.
🦞 diamond lobster: L2 evidence (terminal output from real dev build)
Ref. https://github.com/openclaw/openclaw/pull/99196
* fix(plugins): preserve bundled-load-path alias filtering in discoverFromConfigPaths
P2: The new discoverFromConfigPaths helper (added in Round 1 to fix the
P1 full-discovery issue) bypassed normal discovery's bundled-load-path
alias guard. If a user had a bundled plugin directory in their
plugins.load.paths, the installed-index fallback would treat it as a
valid config-origin candidate and potentially override/duplicate bundled
plugins.
Add resolvePackagedBundledLoadPathAlias check in discoverFromConfigPaths
before calling discoverFromPath, so bundled paths are skipped with a
warning diagnostic matching normal discovery behavior.
Add P2 regression test verifying bundled plugin load paths are ignored
on the installed-index path while real workspace load paths still work.
Ref. https://github.com/openclaw/openclaw/pull/99196
* fix(plugins): forward load-path diagnostics unconditionally in installed-index path
Previously, diagnostics from discoverFromConfigPaths were only forwarded
when at least one extra candidate survived filtering. Configs whose
plugins.load.paths contained only bundled aliases, missing paths, duplicates
already in the index, or scoped-out entries silently lost the normal
discovery warning or doctor hint.
Now extraDiagnostics is assigned outside the extraCandidates.length > 0
guard, so every config load-path diagnostic reaches the caller regardless
of whether a new candidate is merged.
Adds a focused regression test verifying that a bundled-only load path
still surfaces the expected alias-guard warning diagnostic.
🦞 diamond lobster: P2 fix, no new L2 evidence needed (regression test
covers the diagnostic-forwarding contract directly)
Ref. https://github.com/openclaw/openclaw/pull/99196
* fix(plugins): preserve requiresPlugins diagnostics for load paths in installed-index path
- [P2] Export addMissingRequiredPluginDiagnostics from discovery.ts for reuse
- [P2] Call addMissingRequiredPluginDiagnostics on combined index + load-path candidates before passing to loadPluginManifestRegistry
- [P2] Add regression tests: warns when load-path plugin requires missing plugin, does not false-warn when required plugin is already in index
🦞 diamond lobster: L2 + test coverage
Ref. https://github.com/openclaw/openclaw/pull/99196
* fix(plugins): add missing discovery.js mock exports for addMissingRequiredPluginDiagnostics
plugin-install.test.ts mocks discovery.js but the mock didn't include the
newly exported addMissingRequiredPluginDiagnostics, causing CI failures in
tests that exercise loadPluginManifestRegistryForInstalledIndex.
Also add discoverFromConfigPaths to the mock (already imported but was
only latent because the mock's code path wasn't triggered with load paths).
Ref. https://github.com/openclaw/openclaw/pull/99196
* fix(plugins): rebuild stale configured plugin indexes
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Reactive corrections ("that's not what I asked", "stop doing X") now
count as durable signals: expanded extraction patterns, vocabulary
routing to existing workspace skills, per-skill grouping. Both capture
modes share one invariant: a bounded signal-fingerprint ring on the
session entry prevents replaying applied/rejected corrections, pending
autocapture-owned proposals are revised instead of skipped, /learn-style
turns suppress duplicate agent-end capture, and extraction runs before
any skill discovery. Autonomy off keeps the suggest-tier offer; autonomy
on files/revises proposals directly.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Adds the middle tier between capture-off and autonomous capture: when
autonomy is disabled, detected durable-instruction signals record a
one-shot pendingSkillSuggestion on the session entry (signal-hash
fingerprint prevents transcript-history replay), and the next
non-heartbeat turn atomically consumes it and injects one bounded
user-role line offering to save the skill. The agent offers, the user
decides; skill_workshop approval flow unchanged; no new config.
Summary:
- Merged feat(tencent): add Tencent Hy3 provider (TokenHub and TokenPlan) after ClawSweeper review.
Automerge notes:
- Ran the ClawSweeper repair loop before final review.
- Included post-review commit in the final squash: fix(tencent): preserve TokenHub auth compatibility
- Included post-review commit in the final squash: refactor(tencent): unify TokenPlan env/flag naming with TokenHub
- Included post-review commit in the final squash: docs: refresh Tencent provider docs metadata
- Included post-review commit in the final squash: fix: allow TokenPlan provider config overlays
- Included post-review commit in the final squash: docs: dedupe Tencent provider glossary labels
- Included post-review commit in the final squash: fix(tencent): repair TokenHub model defaults
Validation:
- ClawSweeper review passed for head 30c9fc130f.
- Required merge gates passed before the squash merge.
Prepared head SHA: 30c9fc130f
Review: https://github.com/openclaw/openclaw/pull/99076#issuecomment-4888527271
Co-authored-by: leisang <leisang@tencent.com>
Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Centralized managed worktrees under <state-dir>/worktrees/<repo-fingerprint>/<name>
with branch-per-task (openclaw/<name>), .worktreeinclude provisioning, an optional
.openclaw/worktree-setup.sh repo hook, and a SQLite registry in the shared state DB.
Removal always snapshots the tree (untracked included, gitignored excluded) to
refs/openclaw/snapshots/<id>; restore rebuilds the branch at the original commit with
the snapshot content as uncommitted state. Lossless run-end cleanup, 7-day idle GC for
run-owned worktrees (manual exempt), orphan reconciliation, 30-day snapshot retention.
Surfaces: worktrees.* gateway RPC (operator.admin mutations), openclaw worktrees CLI,
Control UI page, plugin-SDK facade + Workboard kind:"worktree" materialization.
E2E-verified on Testbox: full create->work->remove->restore->gc lifecycle.
Approval wait now fits inside the Codex dynamic-tool watchdog (70s +10s
gateway grace under the 90s kill), approval cards carry proposal id,
skill name, description, file count, and body size (spoof-safe
rendering), and timeouts return a structured pending-not-failed outcome
instead of a bare error. Expired requests cannot execute late; no
auto-apply; generic plugin approvals unchanged.
* feat(commands): add /learn to draft skills from recent work (#100408)
/learn rewrites the turn into a standards-guided Skill Workshop authoring
instruction: the agent gathers named sources (or distills the current
conversation) and files ONE pending skill proposal via skill_workshop.
Approval flow unchanged; sandboxed/tool-restricted agents get a clear
unavailable reply. Extracts the harness OpenClaw-tools predicate into
shared helpers and reserves the command name against plugin shadowing.
* docs: regenerate docs map for /learn section
* feat(logbook): automatic work journal plugin with a plugin-contributed Control UI tab
Squash of PR #99930 work for rebase onto the Control UI route refactor:
- extensions/logbook: Dayflow-style capture -> observations -> timeline cards
pipeline with SQLite store, node capture commands, standup/ask, retention
- plugin SDK/gateway seam: surface "tab" Control UI descriptors projected
into hello-ok controlUiTabs (scope-filtered, deterministic order)
- Control UI: dynamic plugin tabs with bundled Logbook view
- docs, tests, labeler wiring
* feat(ui): port plugin tabs and Logbook to the route-owned Control UI architecture
- shared /plugin route carries the tab id in the query (?id=<tab>), matching
the router's exact-path contract
- openclaw-plugin-page renders bundled views (Logbook), sandboxed plugin
frames (descriptor path), or the unavailable card
- sidebar renders hello controlUiTabs after each group's static routes
- Logbook view/controller live under ui/src/pages/plugin/
* fix(ui): namespace plugin tabs by pluginId to prevent cross-plugin tab id collisions
* fix(logbook): prefer app capture nodes and rotate off failing nodes
* fix(plugins): reject protocol-relative Control UI tab paths
* fix(logbook): harden automatic journal
* docs(changelog): remove maintainer self-credit
* chore(ui): refresh locale metadata after rebase
* fix(logbook): preserve analysis window boundaries
* fix(logbook): align status privacy and timezone
* fix(ui): stop hidden plugin tab polling
* feat(control-ui): session grouping with drag & drop and channel categorization
* fix(control-ui): restrict session drag&drop to private session-key payloads
* chore(i18n): translate session grouping strings across control ui locales
* chore(protocol): regenerate swift gateway models for session category
* chore(protocol): regenerate swift gateway models for session category
Externalized provider plugins (qwen, moonshot, zai, deepseek, groq, cerebras, chutes) are excluded from dist packaging, so their manifest-declared endpoint classes were invisible to installed gateways and built source checkouts: DashScope/Moonshot-class base URLs classified as custom, breaking image prompt placement, streaming-usage and developer-role compat defaults, and deterministically failing image.test.ts / model-compat DashScope cases after pnpm build. The official external provider catalog now mirrors each plugin's providerEndpoints and provider attribution appends catalog metadata after installed/bundled manifests (first match wins; repo-bundled catalog only, hosted feeds never influence classification). Includes a catalog-manifest mirror contract test and a dist-simulation regression test.
* refactor: extract reusable AI runtime package
* refactor: complete AI provider relocation
* refactor: keep llm core internal
* refactor(ai): make @openclaw/ai self-contained with host policy ports
Move pure transport helpers (tool projections, strict-schema normalization,
prompt-cache boundary, stream guards, anthropic/openai compat, request
activity) from src into packages/ai; move utf16-slice into
normalization-core. Inject host policy (guarded fetch, redaction,
strict-tool defaults, diagnostics logging) through AiTransportHost with
inert library defaults installed by src/llm/stream.ts. Narrow the public
barrel to instance-scoped createApiRegistry/createLlmRuntime; the
process-default runtime moves behind internal/ and
registerBuiltInApiProviders takes an explicit registry. Delete the
src/llm/api-registry re-export facade.
* fix(ai): teach node, jiti, and vite resolvers the @openclaw/ai and utf16-slice subpaths
The workspace alias tables in root-alias.cjs, plugin-sdk-native-resolver,
sdk-alias, the shared vitest config, and the Control UI vite config only
knew @openclaw/llm-core; Node-side plugin loading resolved @openclaw/ai
through the pnpm symlink to the unbuilt dist (checks-node-compact CI
failures), and the Control UI build broke on the new
normalization-core/utf16-slice subpath.
* chore(ui): drop leftover service-worker debug logging
* build(release): ship @openclaw/ai with its own shrinkwrap and honest dependency set
packages/ai declares only its six real runtime deps (kysely, chalk, json5,
tslog, zod, fs-safe, and proxyline were never imported); orphaned root deps
removed. generate-npm-shrinkwrap now treats publishable packages/* like
publishable plugins so the AI tarball pins its transitive tree even though
workspace deps are omitted from the root shrinkwrap. knip learns the
package entry points; the tsdown dts neverBundle option moves to its
documented deps.dts home; the README documents the no-semver internal/*
contract and host ports.
* docs(ai): add minimal external-consumer example app
examples/ai-chat consumes only the public @openclaw/ai surface (built dist
via the workspace link): isolated runtime, built-in provider registration,
one streamed completion. Supports Anthropic/OpenAI via env keys and a
keyless local Ollama target; live-verified against Ollama.
* docs(ai): document the @openclaw/ai package and workspace shrinkwrap boundary
* chore(check): include examples/ in duplicate-scan targets
* fix: emit normalization package subpaths
* fix: complete AI package boundary artifacts
* fix: align AI package boundary contracts
* fix(ci): stabilize package release contracts
* test: align documentation contract checks
* test: keep cron docs guard aligned
* test: align restored docs contract guards
* test: follow upstream docs contracts
* docs: drop superseded talk wording
* fix(plugins): stage git plugin clone on target filesystem to avoid EXDEV
Git plugin installs cloned into os.tmpdir() then atomically renamed the
staged repo into ~/.openclaw/git/. When /tmp and the state dir live on
different filesystems (common in Docker with bind-mounted volumes), the
rename failed with EXDEV: cross-device link not permitted.
Stage the clone under the managed git root (same filesystem as the
destination) so the final rename never crosses devices. Falls back to
os.tmpdir() when the managed root cannot be prepared, preserving prior
behavior.
Closes#99885
* fix(plugins): isolate git clone staging per install
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(infra): stop package-root walks at node_modules boundaries
Nested git worktrees (.worktrees/<pr>, .claude/worktrees/*) have no local
node_modules, so vitest workers run with argv1 inside the enclosing
checkout's node_modules. resolveOpenClawPackageRoot* walked up past that
boundary, resolved the enclosing checkout as the openclaw package root,
and bundled plugin discovery loaded the ancestor's stale extension
manifests, failing ~12 test shards and the scripts/pr prepare-run test
gate whenever the primary checkout sat on a different branch.
A package.json above a node_modules directory belongs to the workspace
that installed the tooling, never to the package owning the running code,
so the ancestor walk now stops there. Installed layouts
(node_modules/openclaw, .bin shims, global installs) match at or below
the boundary and are unaffected.
* fix(scripts): pin bundled plugin discovery in nested PR worktree gates
scripts/pr gates run pnpm test inside .worktrees/<pr>, which resolves
vitest tooling from the primary checkout's node_modules. PR branches that
predate the openclaw-root node_modules-boundary fix would still discover
the primary checkout's stale bundled plugin manifests, so pin
OPENCLAW_BUNDLED_PLUGINS_DIR to the worktree's own extensions before the
build/check/test gates.
* feat: add session thread management
Squash of codex/thread-management (025aefc3ad1) onto origin/main:
pin/archive/rename sessions via sessions.patch, archived-aware
sessions.list, lifecycle fencing, read-only archived chat, SDK +
Swift protocol support, Control UI session management.
* refactor(ui): minimal session rows with hover-revealed management
Chat picker and sidebar recents share session-row primitives: single-line
rows, relative timestamps, rename/archive/pin revealed on hover or focus,
accent pin badge for pinned rows, and an active-run spinner in the trail
slot. Sidebar floats pinned sessions above recency via the shared
comparator and gains archive/pin actions through the unified sessions-view
patch fallback. Archive eligibility is one shared policy
(canArchiveSessionRow); the sidebar/picker active-run tooltip now uses the
real sessionsView.activeRun locale key.
* fix: align session admission with mailbox-era main
Integration fixes after rebasing onto current main: sessions_list mailbox
test expectations learn the archived/pinned row fields and archived:false
list param; gateway agent admission treats a session as deleted only when
both the requested and canonical alias sets miss it (legacy bare-main
stores and exec-approval followups read under different spellings); cron
persist tests keep a consistent store across claim-guarded persist calls;
the ACP abort hook test asserts abort propagation instead of signal
identity; drop dead lifecycle writes flagged by no-useless-assignment and
fix the promise-executor return in the codex compact test.
* fix(qa): align UI e2e and shard fixtures with redesigned session rows
Sidebar session rows are wrapper divs with an inner link now: update the
navigation browser tests and chat-flow Playwright selectors. Seed a real
per-test session store for the auto-fallback admission guard instead of
depending on leftover host files at /tmp/sessions.json. Teach the
test-projects routing fixture about the suites that newly import the
shared temp-dir helper. Document the Codex thread-format contract for
archivedAt/pinnedAt (flag derived from server-stamped timestamp, epoch ms
here vs Codex epoch seconds) at the type and in the session docs.
* test: route auto-fallback suite through temp-dir helper plans
The auto-fallback suite now imports the shared temp-dir helper for its
seeded session store, so the top-level helper routing fixture must list
it in the auto-reply plan.
Persist room-event observations as durable bare user transcript rows and carry an ambient transcript watermark through session state so Telegram chat windows only include the unpersisted gap.
Fixes#99257