* 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.
* fix(gateway): make doctor dreaming timestamp comparators NaN-safe
compareDreamingEntryByRecency and compareDreamingEntryByPromotion used
`Number.isFinite(aMs) || Number.isFinite(bMs)` then `return bMs - aMs`.
When exactly one side parsed to NaN (non-empty malformed timestamp), the
guard still entered the branch and returned NaN - bMs, producing NaN and
leaving Array.sort order undefined for the doctor.memory.status dreaming
entry list. Coerce unparseable timestamps to -Infinity via a shared
parseDreamingTimestampMs helper and return a finite -1/0/1 from the
timestamp branch so the sort stays deterministic.
* fix(memory-core): incorporate producer-side NaN-safe timestamp comparator
ClawSweeper [P1] finding: gateway-only fix is incomplete because Memory Core
has the same mixed-NaN comparator BEFORE the gateway merges lists. A valid
entry discarded upstream cannot be recovered at the gateway boundary.
Incorporate the producer-side fix:
- Export compareStoreTimestampDesc from short-term-promotion-utils.ts
- Replace the buggy raw Date.parse + bMs-aMs pattern in
short-term-promotion-stats.ts with the NaN-safe compareStoreTimestampDesc
This makes #118749 self-contained: both gateway doctor.ts AND memory-core
stats.ts comparators are NaN-safe. The companion #118750 now only needs to
cover the recency ranking fix (ageDays/recency calculation).
* fix(memory): preserve valid dreaming diagnostics
* style(memory): format dreaming imports
---------
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
* fix(memory-core): fail closed on non-ENOENT companion stat errors
Only treat ENOENT as 'sidecar is absent'. Other fs errors (EACCES, EIO,
ELOOP, EMFILE) now return null (fail-closed) instead of silently treating
the sidecar as zero-byte, preventing legacy data from being dropped on
transient or permission failures.
* fix(memory): silence self-inflicted legacy sidecar migration warning
OpenClaw can leave a zero-byte placeholder at the legacy memory sidecar
path (~/.openclaw/memory/<agentId>.sqlite) while the live memory index
is the per-agent SQLite database. The legacy-index migration check found
the empty file, attached it, saw no legacy schema, and warned on every
CLI invocation and gateway start, with no way for users to clear it.
Treat a demonstrably empty sidecar (zero-byte main file and no WAL or
journal content) as nothing to import: remove it quietly as a migration
change instead of emitting a permanent warning. Sidecars that contain
any bytes still go through the existing import and warning paths, so a
real legacy index or a genuinely unrecognized database is unaffected.
Closes#114626
* fix(memory-core): fail closed on non-ENOENT companion stat errors
Only treat ENOENT as 'sidecar is absent'. Other fs errors (EACCES, EIO,
ELOOP, EMFILE) now return null (fail-closed) instead of silently treating
the sidecar as zero-byte, preventing legacy data from being dropped on
transient or permission failures.
* test(memory-core): add ELOOP and EACCES companion stat regression coverage
Regression tests for the fail-closed fix: when a WAL/SHM/journal
companion's fs.stat fails with ELOOP (self-referential symlink) or
EACCES (permission denied), the migration must NOT remove the main
legacy sidecar file. Only ENOENT means genuinely absent.
Addresses Whatsskill Codex review P1 finding on openclaw/openclaw#114661.
* ci: retrigger after transient runner failures