* test(control-ui): add --operator-scopes flag to the mock dev server
* feat(control-ui): simplified settings experience for non-admin operators
Non-admin browsers previously saw every settings page, many of which
dead-ended or rendered enabled controls whose RPCs fail with
'missing scope: operator.admin'.
- config.schema drops from operator.admin to operator.read: the schema is a
static document describing options whose values are already readable via
read-scoped config.get; admin-only schema only broke read-only settings
rendering (Automation/Infrastructure/AI Agents/Communications showed
'Schema unavailable. Use Raw.').
- Settings sidebar and settings search hide admin-only routes (custodian,
labs, updates, automation, infrastructure, mcp, security, secrets,
cloud-workers, communications, ai-agents, model-setup) for non-admin
viewers; legacy gateways without advertised scopes keep the full UI.
- Channels, Devices, Worktrees, Memory Import, Profile gate their mutation
controls on actual scopes with 'Browsing only…' notices instead of
enabled-but-failing buttons; Devices no longer fires device.pair.list /
exec.approvals.get without the scopes to call them (kills the two red
error callouts on page load).
- Scope-upgrade banner: dismissing it in the guidance phase (no in-app
upgrade path) now hides it fully instead of leaving a permanent chip.
- Config write coordinator surfaces scope refusals as a visible
admin-required error instead of silently resolving false.
* test(control-ui): advertise config.schema in the mock dev gateway
ensureSchemaLoaded now checks method advertisement + scope before loading
the schema; the mock harness must advertise config.schema like a real
gateway does or schema-driven settings pages render empty in the mock.
* fix(control-ui): close the worktree create draft on scope downgrade
* perf(doctor): isolate memory health artifact
Doctor lint loaded the broad Memory Core API barrel only to register health checks and read isolated check IDs. That synchronously pulled the full memory public graph into the first lint run, consuming most of the 120-second test budget.
Load a dedicated doctor-health public artifact instead and verify it is packaged. The bisect boundary was 9de3ca5fc9 (#125571); because that commit only adds upgrade-test assets, it exposed a pre-existing runner-sensitive cost rather than introducing the expensive import path.
* test(control-ui): restore device lifecycle test boundary
* perf(control-ui): lazy-load settings sidebar
* fix(ui): recheck access after confirmations
* fix(control-ui): gate presence-driven device reloads on pairing access
The presence connectivity-change path still called device.pair.list without
operator.pairing, the same invariant the pair-event and poller paths already
guard; a limited browser got a doomed RPC on every connectivity change.
* fix(control-ui): fail open on schema loads for legacy scope-less gateways
canCallGatewayMethod hardened to strict advertisement+scope checks (#125478),
which made the new ensureSchemaLoaded gate silently skip config.schema for
legacy hellos without advertised scopes or a method list. Schema loads now
skip only on a definitive denial (method advertised absent, or advertised
scopes without operator.read), reusing the fail-open hasOperatorReadAccess
semantics the rest of the non-admin UI uses; regression test pins the
legacy snapshot path.
* test(control-ui): split schema-access coverage into its own file
runtime-config-capability.test.ts crossed the max-lines cap; the legacy
fail-open regression and its denial counterpart move to a colocated
schema-access test file.
* fix(scripts): keep mapped Vitest lanes at their measured no-output floor
The codex extension shard legitimately works in silence beyond 300s under
the default reporter (measured 61s import + 293s testing at ~95% CPU); the
CI-wide OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=300000 env override shrank the
lane below that and the watchdog killed healthy runs, flipping with
incidental flake output (#125825). Per-config entries in
VITEST_CONFIG_NO_OUTPUT_TIMEOUT_MS now act as measured silence floors: a
global env value may widen a mapped lane's window but no longer shrinks it;
unmapped configs and the explicit '0' disable keep env verbatim. Adds the
codex extension lane to the map at the extra-long tier (same class as the
discord entry from #123025).
Linux signal-zero probes succeed for zombie processes. Reclaim memory promotion and session usage locks only when their exact zombie owner is still current, and route shared test waits through the canonical zombie-aware PID helper.
memory_search corpus=all silently swallowed a returned memory-manager
error (missing sqlite, embedding misconfiguration, corrupted index):
the error branch was gated on !shouldQuerySupplements, so the tool
served wiki-only results in a payload with no error/warning field and
recorded no cooldown — a degraded search read as complete, contradicting
the tool description's own disabled-flag contract. The thrown-error path
already surfaced this; only the returned-error path was silent.
Record the cooldown in both cases and, when supplements still serve,
carry a warning naming the unavailable memory corpus in the payload.
Move llama.cpp chat and local embeddings onto a verified externally managed llama-server runtime. Remove the in-process native runtime, forked embedding workers, and node-llama-cpp dependency while preserving guided setup, local GGUF models, tool-capable agent runs, diagnostics, and operator docs.
Replace assertion-driven cleanup with real runtime narrowing and typed owner contracts across memory-core dreaming.
Reject the unsound one-file lint ratchet and document repository-wide rules against checker-gaming and baseline-driven enforcement.
Co-authored-by: Amp <amp@ampcode.com>
* refactor(memory-core): split index manager along the inheritance chain
* fix(memory): restore the manager lazy boundary
* refactor(memory-core): delete dead manager plumbing
* chore(lint): ratchet max-lines baseline after memory manager split
* fix(memory): keep the manager out of the runtime facade
* test(memory): cover transitive manager facade imports
* fix(memory): prune stale session index rows at startup
* fix(memory): reconcile stale sessions without re-embedding
* oc-9df: fail closed on session corpus scan errors
---------
Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com>
* feat(memory): support globbed extra paths
Allow root-relative glob patterns on existing memory.search.extraPaths entries across discovery, watching, reindex identity, and QMD migration.
* fix(memory): drop stale lease type import
* fix(memory): enforce extra path glob scope
* 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(doctor): keep bundled doctor contract closures dependency-light
Doctor contract enumeration cold-loads each plugin's doctor-contract-api
closure via jiti, so a static value import of openclaw/plugin-sdk/runtime-doctor
pulled the state-db/kysely graph (~4.3s per closure) into
listPluginDoctorLegacyConfigRules / listPluginDoctorStateMigrationEntries.
- migrate all light doctor-contract closures (66 files) to the
dependency-light openclaw/plugin-sdk/runtime-doctor-migrations subpath
- voice-call: load detect/repairOpenClawStateDatabaseSchema* lazily inside
the migration bodies; keep only a type-only static runtime-doctor import
- matrix: split pure credential record shapes/normalizers into
credentials-state.ts so the doctor closure no longer imports the sync
plugin-state store through credentials-read
- guard: doctor-contract-closure-guard.test.ts now forbids static value
imports of runtime-doctor in closures alongside agent-runtime
* fix(matrix): keep credential revocation record type module-local
Knip production scan flags the export as consumer-less; the type is only
referenced by the exported union and revocation guard signature.
* 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.