* fix(plugins): keep OpenCode Go bundled
* fix(plugins): mark OpenCode Go dist bundled
* fix(docs): show OpenCode Go as bundled
* fix(release): defer bundled plugin publication
* refactor(plugin-sdk): delete the heavy runtime-doctor barrel
Nothing may pull the state-db/kysely graph through a doctor barrel anymore.
The barrel's remaining heavy exports move to two narrow private-local
subpaths, each with a single purpose:
- doctor-repair-runtime: install-path diagnosis, plugin config removal, and
state-database schema detect/repair (matrix doctor, voice-call lazy import)
- plugin-state-store-runtime: the sync keyed-store factory. It stays out of
plugin-state-runtime because hot channel entrypoints import that at module
load and opening a store pulls the state-database graph.
Doctor closures also stop pulling ssrf-runtime (fetch-guard + gateway net)
for two legacy private-network helpers that live in the lighter ssrf-policy
subpath: mattermost, nextcloud-talk, tlon, matrix.
The closure guard now forbids the two new heavy subpaths instead of the
deleted barrel, so the invariant keeps being enforced where it still applies.
* perf(doctor): keep heavy graphs out of every doctor closure
Doctor enumeration cold-loads each declaring plugin's contract closure, so
one heavy import in a closure is paid by the whole sweep. Four barrels were
still dragging unrelated graphs in for trivial helpers; each is repaired at
the leaf rather than by caching downstream:
- Legacy private-network config migration moves to a config leaf. It only
reshapes records, but lived beside the SSRF runtime (DNS, proxy, logging),
costing mattermost ~2.7s. ssrf-policy re-exports it, surface unchanged.
- Streaming config readers move to a leaf. They read two config keys, but
streaming.ts also formats tool aggregates, pulling tool-display/logging/
acp-core; that cost slack ~2.3s.
- signal took the channel-secret barrel for isRecord; the canonical plugin
record guard is string-coerce-runtime (root AGENTS.md).
- llm-task took the provider-model barrel for parseModelRef, now a narrow
model-ref-parse subpath.
Full doctor enumeration of all 42 declaring plugins, built mode:
legacy config rules 6668ms -> 1265ms, state migrations 184ms -> 127ms.
No plugin remains an outlier; the slowest is now ~380ms against a ~200ms floor.
Public export surfaces of every touched SDK subpath are byte-identical
(verified by diffing built module exports before/after); the API baseline
hashes move only because re-exported declarations emit differently.
The closure guard gains rules for each repaired barrel so the invariant
holds for future closures.
* fix(release): exclude new private-local declarations from the published package
Same pack-path rule as c41da3759f: private-local subpaths ship without d.ts.
* fix(doctor): repair the closure guard violations that break main
The landed guard fails on main: three closures import heavy barrels for one
symbol each. Two more surfaced once the guard learned about the provider-model
barrel. Each gets a narrow subpath at the leaf:
- telegram sent-message-cache + state-migrations took the session-store barrel
(session accessor + state-db) for resolveStorePath -> session-store-paths
- discord thread-bindings.state took the channel-outbound barrel (reply
pipeline + channel registry) for one identity write -> outbound-echo-runtime
- discord model-picker took the provider-model barrel for normalizeProviderId,
which model-ref-parse now exposes beside parseModelRef
The guard also stops walking artifacts of plugins whose manifest declares no
doctor surface. Such a declaration gates the artifact off every enumeration
path exactly as resolvePluginDoctorContracts does, so its closure cost is never
paid; anthropic ("doctorContract": {}) was being held to a cost it cannot
incur. Absent declarations still load eagerly and stay enforced.
Side effect worth naming: discord's built doctor contract now loads again.
On main both discord and telegram fail to require in packaged builds (an
ESM-only transitive dep) and silently lose their repairs; this restores
discord and takes enumerated legacy config rules from 87 to 99. Telegram's
built artifact still pulls execa through dist chunking - a build-level defect
with a different owner, filed as follow-up.
* perf(doctor): slim remaining heavy doctor contract closures
Follow-up to #120698: several doctor closures still cold-loaded multi-second
kysely-bearing graphs through other broad barrels (session-store-runtime,
realtime-voice, channel-outbound, logging-core, memory-host-core/-events,
sqlite-runtime, persistent-dedupe, and plugin-local barrels).
- lazy-import heavy helpers inside async migration bodies (codex, msteams,
zalouser, workboard, matrix inbound-dedupe, memory-core migrations)
- bypass plugin-local barrels to defining modules (reef protocol,
memory-core short-term-promotion)
- move to lighter existing subpaths (slack -> channel-streaming, matrix
logger -> security-runtime, memory-wiki -> agent-scope-runtime, which now
also exports resolveSessionAgentId)
- add narrow openclaw/plugin-sdk/realtime-voice-activation for discord's
sync wake-name doctor rules
- split src/infra/kysely-sync-cache-state.ts so sqlite-transaction clears
Kysely caches without value-loading kysely; split the memory-host-sdk
kysely bridge off the schema/transaction bridge
- guard: forbid the heavy barrels in doctor closures with per-kind scoping
Cold enumeration per plugin: discord 52.6s->0.3s, msteams 30.9s->0.5s,
codex 29.6s->2.6s, zalouser 28.8s->2.3s, matrix 27.2s->3.2s,
slack 17.5s->1.5s, reef 9.9s->0.7s, memory-core 6.4s->3.6s,
workboard 3.4s->0.25s; all kysely-free except llm-task (named follow-up).
* fix(plugins): route slack streaming compat through a focused streaming-config subpath
The channel-streaming compat barrel is deprecated for extension production
code (deprecated-api-usage guard + SDK package contract). Add the narrow
non-deprecated openclaw/plugin-sdk/channel-streaming-config subpath for the
pure streaming config readers, and drop the now consumer-less
short-term-promotion barrel re-exports knip flagged.
* test(plugins): register memory-host-sdk kysely bridge in package boundary inventory
* fix(plugins): classify realtime-voice-activation as private-local
ClawSweeper P2: the subpath exports only a default target, which is the
private-local shape; register it in plugin-sdk-private-local-only-subpaths,
the package-boundary d.ts alias maps, and correct the public surface budgets
(realtime-voice-activation no longer counts as public).
* fix(release): exclude realtime-voice-activation declarations from the published package
Private-local subpaths ship without d.ts; register the files negation the
release pack-path check requires.
* perf(plugins): declare doctor contract surfaces
* perf(doctor): slim migration import closures
* perf(plugins): narrow doctor declaration record surface and wire owner-test lane
Registry records carry only the doctorContract declaration instead of the whole
parsed manifest, and check:changed now selects the src/plugins-owned declaration
honesty and closure-guard tests for extension module/manifest changes so
cross-lane drift cannot pass PR classification.
* fix(doctor): keep control-plane dist imports require-safe
Keep doctor and channel control-plane chunks off exec-class dependencies, and enforce native require(esm) loading during postbuild.
* chore(plugin-sdk): regenerate API baseline
* chore(plugin-sdk): sync export ordering
* fix(plugins): satisfy doctor contract CI boundaries
* perf(doctor): make qqbot doctor closure dependency-light
qqbot was the last plugin above 5s in doctor state-migration enumeration
(~8s under tsx/jiti). The cost was not the state-key builder (already a
leaf): its doctor closure value-imported the runtime-doctor SDK barrel,
whose plugin-state-store/state-db re-exports pull kysely (~330 modules),
plus security-runtime for one fileExists (~200 modules), all resolved
per-module by jiti during enumeration.
Split the migration-define helpers and light re-exports into a new
private-local plugin-sdk/runtime-doctor-migrations subpath; runtime-doctor
re-exports it so its public surface is byte-identical (API baseline hash
unchanged). qqbot's doctor-contract and state-migrations now import only
the light subpath, swapping fileExists for the equivalent async
legacyStateFileExists already in the closure.
qqbot enumeration: ~8.0s/531 modules -> ~0.25s/18 modules.
* chore(plugin-sdk): drop private-local subpath from API baseline
runtime-doctor-migrations is private-local-only; the baseline tracks public
modules, and the earlier line was generated before the classification.
* fix(plugins): register runtime-doctor-migrations boundary paths
The private-local subpath list feeds the extension package boundary map;
the shared paths config and xai's derived overrides must carry the same
entry or the boundary contract test fails.
* feat(update): scheduled update campaigns with countdown, dev-channel auto-update, and Updates settings page
* fix(ui): satisfy update campaign deadcode checks
* feat(update): extend scheduled update campaigns
* test(ui): include Updates page in settings sidebar search expectations
* fix(update): end campaigns when apply fails
* fix(update): pin dev campaign applies to the announced commit
* test(ui): align update e2e fixtures
* fix(update): pin adopted dev updates to the announced commit
* docs(update): document automatic dev update campaigns
* fix(ui): gate automatic dev updates on git installs
* fix(gateway): scope detailed update metadata to read-capable clients
* fix(gateway): break post-attach madge cycle with a leaf client-lookup type
* chore(protocol): regenerate clients after rebase
* test(gateway): update method tail after rebase
* fix(update): preserve campaign target and hold state
* test(update): align adoption mocks and split handoff suite
* test(update): route split handoff suites on Windows
* [AI] fix(diagnostics-otel): keep telemetry exporting across in-process restarts
Owned diagnostics-otel generations previously registered their OpenTelemetry
providers globally and only shut the providers down on stop. The pinned
sdk-node registers globals without override and never unregisters them, so a
second in-process generation (config-watcher reload, OPENCLAW_NO_RESPAWN=1,
containers) kept exporting through the first, already-shutdown providers and
telemetry silently stopped.
Switch owned mode to private BasicTracerProvider + MeterProvider instances
whose handles are injected directly into the existing recorder runtime, and
keep the preloaded (OPENCLAW_OTEL_PRELOADED=1) mode on the host's global
providers without ever registering or replacing globals. Resource detection
honors the pinned NodeSDK OTEL_NODE_RESOURCE_DETECTORS contract (unset
defaults to env+process+host; none/subset/all are respected), and the unused
@opentelemetry/sdk-node dependency is removed with a minimal lockfile update.
Adds a real-SDK two-generation restart regression, real-SDK resource-detector
selection coverage, migrates the unit mocks to the provider lifecycle, and
updates the OpenTelemetry docs.
Fixes#119997
Co-Authored-By: glm-5.2 <noreply@anthropic.com>
* [AI] test(diagnostics-otel): restore OpenTelemetry globals after restart test
The restart regression disables global context, metrics, propagation, and
trace APIs and forces OPENCLAW_OTEL_PRELOADED=0, but its cleanup only reset
diagnostic events, so a later test in the same worker could inherit no-op
providers. Snapshot the prior global registrations and preloaded env at
module load, and re-register/restore them in afterEach, matching the
existing exporter-health integration test pattern.
Co-Authored-By: glm-5.2 <noreply@anthropic.com>
* [AI] fix(diagnostics-otel): reconcile private providers with OTEL_SDK_DISABLED
Rebase onto main after #119961 (honor OTEL_SDK_DISABLED) changed the
disabled admission path to return before SDK construction. Restore the
removed getBooleanFromEnv import, drop the stale ownedNodeSdkDisabled
guard, and update disabled-mode tests to the new semantics (all routes
off, disabled runtime registered) plus the private-provider signal
path for the integration suite.
Related to #119997
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(diagnostics-otel): isolate restartable provider generations
* test(diagnostics-otel): prove same-pid generation routing
* test(diagnostics-otel): use supported watcher reload mode
* test(diagnostics-otel): satisfy ownership proof gates
* test(diagnostics-otel): use managed proof cleanup
* test(diagnostics-otel): verify injected trace ancestry
---------
Co-authored-by: glm-5.2 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix: gateway service commands refuse a named profile or relocated OPENCLAW_HOME
- Resolve the default install identity against the canonical state directory
for the active OpenClaw home and profile instead of the unprofiled OS
account default.
- `--profile <name>` / `--dev` project `.openclaw-<profile>` state and config
paths, so every named profile was classified as isolated state and refused
`install`, `start`, `stop`, `restart`, `uninstall`, Doctor service repair,
and self-update service handling.
- `OPENCLAW_HOME` relocates all OpenClaw path defaults and is documented for
running as a dedicated service user; a relocated home is now an install
identity. `HOME` alone still is not.
- An `OPENCLAW_STATE_DIR` or `OPENCLAW_CONFIG_PATH` pointing outside those
canonical paths is still treated as isolated state.
- Recovery guidance in the refusal message now names the paths that must match.
Verified: focused vitest shards for the changed suites plus the daemon, CLI,
and doctor suites that consume the identity check; tsgo core and core-test
lanes; oxlint; docs format, MDX, link, and map checks.
* fix(gateway): keep relocated homes isolated
* fix(config): validate service profile identity
* fix(daemon): enforce named-profile service ownership
* fix(update): reject drifted service selectors before probes
* test(windows): prove scheduled task lifecycle
* test(windows): harden scheduled task proof cleanup
* test(windows): bind lifecycle proof to checkout
* test(windows): normalize cleanup exit status
* test(windows): verify effective task privilege
* test(windows): protect scheduled task proof roots
* test(windows): prove listener-owned task lifecycle
* test(windows): fix scheduled task proof contracts
* test(windows): remove redundant mock coercions
* test(windows): measure fallback before task probes
* test(windows): prove scheduled task process origin
* fix(gateway): preserve unmanaged restart fallback
* test(gateway): cover denied restart ownership
* test(gateway): keep restart helper types private
* test(gateway): classify lifecycle helpers as test code
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>