feat(agent-memory): Phase 7 — discovery marker#255
Conversation
5cdeb4c to
c317978
Compare
524f3a8 to
e58dbb7
Compare
c317978 to
d5a95b9
Compare
e58dbb7 to
0d89d67
Compare
d5a95b9 to
e27477f
Compare
0d89d67 to
73d91d7
Compare
KIvanow
left a comment
There was a problem hiding this comment.
Phase 7 is cleanly additive: agent-cache stays behaviorally identical, discovery is opt-in, the {name}:mem field namespacing is a nice touch, and close() awaiting discoveryReady before teardown is the right shape. One thing I would like reworked, plus two minors:
1. The cross-type collision protection is both unreachable for the case the PR cites and silent if it ever fires. The description says it "detects a name collision with a different cache type and throws," but:
- Memory registers under field
{name}:memwhile agent-cache registers under{name}(seeagent-cache/src/discovery.ts:210). Those are different hash fields, so a memory-vs-cache same-name collision can never land on the same field andcheckCollisionnever sees it. The only way the check fires is if something wrote a non-agent_memorytype into{name}:mem, which nothing in the codebase does. - Even if it did fire, the throw is swallowed twice: registration is fire-and-forget with
ready.catch(() => undefined), andclose()doesawait this.discoveryReady.catch(() => undefined). No caller path observes it, so the store just constructs and runs with the marker silently unwritten. Agent-cache at least logs on collision/overwrite. Please route a collision throughonWriteFailedor a visible warning, and align the PR wording with what actually happens.
2. (minor) require('../package.json') runs at module load for every importer of MemoryStore, even with discovery disabled. It is a top-level const, so every consumer pays a disk read on import and inherits a bundler hazard (package.json is not always emitted). Reading it lazily inside createDiscovery scopes the cost to discovery users.
3. (minor) tickHeartbeat re-issues SET PROTOCOL_KEY ... NX on every interval, which is a guaranteed no-op after the first tick and can be dropped from the heartbeat path. The HGET to HSET collision check is also non-atomic (TOCTOU), which is low-stakes for best-effort discovery but worth a comment.
e27477f to
6864f6b
Compare
73d91d7 to
8bc0c13
Compare
|
@KIvanow Reworked in 8bc0c13: (1) a cross-type collision now emits a visible |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8bc0c13. Configure here.
6864f6b to
3ee71c8
Compare
8bc0c13 to
9f8485a
Compare
3ee71c8 to
1b71d3e
Compare
94d2a7d to
66c0809
Compare
- MemoryDiscovery registers an agent_memory marker on the shared __betterdb:caches registry: type 'agent_memory', prefix, version, capabilities ['recall','consolidate','reinforce'], stats_key - Reuses the agent-cache discovery protocol constants (additively re-exported) so Monitor reads memory markers identically; writes via MemoryStore's .call client (no method-style client needed) - Heartbeat on an unref'd interval with a TTL'd heartbeat key; best-effort writes; name-collision detection against a different cache type - MemoryStore gains an opt-in discovery option and close() to stop the heartbeat and remove the marker - Re-export discovery constants/MarkerMetadata from @betterdb/agent-cache
…:mem
Register the agent_memory marker under the {name}:mem registry field and
heartbeat key so a memory store and an agent-cache sharing the same name
no longer collide on the same __betterdb:caches field / heartbeat key
(reported on the AgentMemory facade, which discovers both tiers).
…at work
- a cross-type marker collision now emits a visible console.warn and proceeds
last-writer-wins, instead of throwing into a swallowed registration promise
(the memory marker lives under {name}:mem, so it never collides with
agent-cache's {name} field — the check is purely defensive)
- read package.json lazily inside createDiscovery so non-discovery importers
don't pay a disk read (and avoid the bundler-emit hazard) at module load
- drop the redundant SET protocol NX from every heartbeat tick (no-op after
register); note the best-effort HGET->HSET TOCTOU
…tes it stop() cleared the interval and DEL'd the heartbeat but didn't wait for a tick already running, so a tick that fired just before close() could re-write the heartbeat/marker after the DEL and make the store look alive post-shutdown. Track the in-flight tick and await it before deleting.
66c0809 to
c83f1e6
Compare

Stacked on #254 (Phase 6 — consolidate).
What
Phase 7 of
@betterdb/agent-memory: a discovery marker so BetterDB Monitor auto-discovers memory stores on any Valkey it watches, reusing the shared registry protocol from agent-cache.MemoryDiscoveryregisters a marker on__betterdb:cacheskeyed by store name:type: 'agent_memory',prefix,version,protocol_version,capabilities: ['recall','consolidate','reinforce'],stats_key: {name}:__mem_stats, plusstarted_at/pid/hostname.register()sets__betterdb:protocolwithNX; heartbeats then run on an unref'd interval, refreshing a TTL'd__betterdb:heartbeat:{name}key and the marker (theNXprotocol set is not re-issued per tick — it's a no-op after the first).{name}:mem, distinct from agent-cache's{name}, so the two never collide here. If a foreign-type marker is ever found in this field it emits a visible warning and proceeds last-writer-wins (rather than throwing into a swallowed promise); a same-type marker is overwritten.MemoryStoregains an opt-indiscoveryoption (registers on construct) andclose()to stop the heartbeat and delete the marker.Design / reuse notes
DiscoveryManageruses a method-styleValkeyclient and hard-codes its cache type in the collision check, whileMemoryStoreis built on a.call-only client. Rather than refactor the merged manager (and its FakeClient tests), this reuses the protocol as the single source of truth — agent-cache now additively re-exports its discovery constants (REGISTRY_KEY,PROTOCOL_KEY,HEARTBEAT_KEY_PREFIX, TTL/interval,PROTOCOL_VERSION,MarkerMetadata) — and implements the register/heartbeat loop against.call, so Monitor reads the memory marker identically to a cache marker.close()awaits readiness before teardown.onWriteFailedhook is in place for the Phase 9 observability wiring).Tests
discovery.test.ts(8): marker shape/capabilities/stats_key · protocolNX+ heartbeat TTL · cross-type collision warns and overwrites (last-writer-wins) · same-type overwrite ·stopdeletes heartbeat ·tickHeartbeatre-writes · interval fires (fake timers) · best-effort on write failure.MemoryStore.discovery.test.ts(2): opt-in registers on construct +close()tears down · disabled → no registry I/O.@betterdb/agent-cacheunchanged behaviorally (253/253 still green after the additive export). agent-memory: 65/65 green ·tscclean · prettier clean.Note
Low Risk
Best-effort optional discovery metadata on Valkey; no changes to recall/remember/consolidate paths or agent-cache runtime behavior.
Overview
Adds opt-in Valkey discovery for
@betterdb/agent-memoryso BetterDB Monitor can find memory stores the same way it finds agent caches.MemoryDiscoverywrites anagent_memorymarker to the shared__betterdb:cachesregistry (field{name}:mem, separate from agent-cache’s{name}), sets__betterdb:protocolonce withNX, and runs an unref’d heartbeat that refreshes a TTL’d heartbeat key and re-writes the marker. Cross-type collisions warn and last-writer-wins; registry/heartbeat I/O is best-effort (onWriteFailedhook for later observability).MemoryStoreacceptsdiscovery?: boolean | MemoryDiscoveryConfig, registers on construct (fire-and-forget, sync constructor), andclose()awaits registration then stops heartbeats and deletes the heartbeat key.@betterdb/agent-cacheonly additively re-exports discovery protocol constants (REGISTRY_KEY,PROTOCOL_KEY, heartbeat TTL/interval,MarkerMetadata, etc.)—no behavior change to existing discovery.New unit tests cover marker shape, protocol/heartbeat behavior, stop/teardown ordering, and
MemoryStorewiring when discovery is on vs off.Reviewed by Cursor Bugbot for commit c83f1e6. Bugbot is set up for automated code reviews on this repo. Configure here.