Files
Dragan Spiridonov 358990cc01 fix(kernel,learning): namespace-aware reads + timestamp rehydration (#491 Bug 2+3)
Issue #491 — even with the worker→kernel wiring restored (Bug 1/4b),
`mineExperiences` mined 0 for every domain on installs with hundreds
of indexed experiences. Two stacked bugs in the learning read path:

Bug 2 — namespace mismatch (the keystone)
=========================================
LearningCoordinator writes every experience with
`namespace: 'learning-optimization'`, but the MemoryBackend
interface only let writers specify a namespace; reads had no such
option. HybridBackend.get/search/has/delete therefore hard-coded
`defaultNamespace='default'`. The coordinator could not read back
its own data — `learning:experience:index:domain:<d>:*` returned 0
keys even when the DB held 81+ matching rows in the
`learning-optimization` namespace.

Fix:
  - Add `RetrieveOptions { namespace? }` to kernel/interfaces.ts
    (symmetric with the existing `StoreOptions.namespace`).
  - Extend MemoryBackend.get/has/delete/search to accept the option
    (backward-compatible — undefined still routes to defaultNamespace).
  - HybridBackend honors the option in all four methods.
  - InMemoryBackend likewise; its `search` now also scopes the scan
    by namespace prefix when one is supplied, so unit-test fakes
    behave the same as the production hybrid backend.
  - All 15 read sites in LearningCoordinator pass
    `LEARNING_NS = { namespace: 'learning-optimization' }`, matching
    the writes that already specified it.

One pre-existing unit test (memory-backend.test.ts) was calling
`backend.get(key, 'test-ns')` against the undocumented positional
namespace param InMemoryBackend used to accept. Migrated it to the
options form — the positional shape was never part of the
MemoryBackend interface.

Bug 3 — Date vs ISO-string in time-window filter
================================================
`recordExperience` stores `timestamp: new Date()`. JSON.stringify
serializes it to an ISO string and JSON.parse does not rehydrate.
`TimeRange.contains` does `date >= start`; in `string >= Date`, the
string coerces to NaN and every comparison is false — so even with
Bug 2 fixed, the window filter would still silently drop every row.

Fix: in `getExperiencesByDomainAndTime`, coerce
`experience.timestamp` to a real `Date` at the kv-read boundary
before calling `timeRange.contains`. Downstream callers receive a
hydrated Experience with a proper Date. We deliberately fix this at
the boundary rather than making `TimeRange.contains` tolerant of
strings — the bug is "kv round-trip drops the Date type", not "the
value object is wrong", and a tolerant comparator would mask future
type drift everywhere else.

Round-trip integration test:
tests/integration/learning/coordinator-roundtrip.test.ts
pins three assertions:
  1. recordExperience → mineExperiences finds the experience back
     (this returned 0 before either fix).
  2. The mined experience's timestamp is a real Date instance, not a
     string (proves Bug 3 fix is at the boundary).
  3. A time window deliberately in the past still excludes recent
     writes (proves we didn't fix Bug 3 by making the filter a
     no-op).

Verified red→green: with Bugs 2+3 reverted (interface kept), tests
1 and 2 fail with `experienceCount=0` and `rows.length=0` — exactly
the symptoms from the issue. With both fixes in place, 3/3 pass in
~30ms. 466/466 kernel unit tests and 335/335 adjacent suites stay
green; the only test that needed migration is documented above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 08:46:53 +00:00
..