Add observability for memory + agent-loop lifecycle signals#18
Conversation
Memory and several agent-loop signals were completely silent — no logs, no notifications, no programmatic surface — while the skills subsystem already had a full event pipeline. This brings memory and the loop up to parity using the same proven notifier pattern. Memory (internal/memory): - New notifier.go: MemoryEvent + MemoryNotifier interface + Noop/Multi implementations, mirroring skills/notifier.go. - MemoryManager.SetNotifier propagates to the EpisodeStore so facts AND episodes share one sink. - Fire events at every lifecycle point: fact_added (skips silent dedups), fact_merged (merge-on-write, with similarity), fact_replaced, fact_removed, fact_consolidated (before→after), episode_stored (carries Untrusted), episode_deduped, episode_evicted (TTL/cap, batch), episode_promoted, episode_pending_review. - Drive-by fix: AddFact's merge-on-write branch now marks the system prompt dirty so a merged fact is visible on the next turn. Agent loop (internal/loop): - New signal.go: SignalEvent + SignalHandler + Engine.SetSignalHandler. - Emit context_trimmed (proactive budget trim + post-error survival trim) and tool_recovery (repeated tool failure → corrective hint). Surfaces (all four): - Terminal: render.Renderer memory/episode/signal methods, gated behind a new memoryVerbose flag (enabled by verbose interaction mode). - Programmatic: Config.MemoryEventHandler + Config.AgentSignalHandler with adapters that fan out alongside the renderer. - Web UI: serve.go streams memory_event/agent_signal over the WebSocket; app.js dispatches them to toasts (noisy events kept silent). - Telegram: verbose-gated chat notifications for meaningful events. Tests: notifier_test.go (facts/episodes, dedup-is-silent, propagation), signal_test.go, and render tests for the new methods. Docs updated (MEMORY.md observability section, AGENTS.md source layout).
Verification-protocol pass on the memory observability change surfaced a structural finding plus two coverage gaps; this commit repairs them. Finding (axis 2.4/2.5 — callback under lock): memory lifecycle events were delivered while internal locks were held — EpisodeStore.notify under e.mu (writeLocked/Prune/Promote) and the fact events under the shared per-dir facts lock. A notifier runs arbitrary caller code (a WebSocket send under `odek serve`, or a handler that re-enters the store), so firing under the lock serialized writes behind the sink and risked a reentrancy deadlock. Fix (behavior-preserving): collect events while locked and fan them out after unlocking. - EpisodeStore.writeLocked now returns []MemoryEvent; WriteWithProvenance, Prune, and Promote fire via the new notifyAll once e.mu is released. - MemoryManager fact methods collect into a pending slice fired by fireAfterUnlock after the facts-dir lock is released. Coverage repairs (additive): - Test the merge-on-write fact_merged path (previously untested), with a regression guard that the merged fact reaches the system prompt (markPromptDirty fix on that path). - README: note the new memory observability surface (axis 2.9 doc gap). Verified: full `go test ./...` green; `go test -race` clean on memory and loop; go vet + gofmt clean.
📜 Verification Certificate — AI Verification Protocol v5.2.7Ran the AI Verification Protocol against this PR's diff (acting as the B/C/D/E pipeline) and auto-repaired the safe/additive findings. Repairs landed in Classification: Axes Summary
Primary Finding (🔴 → fixed)Memory lifecycle events were delivered while internal locks were held — Repair (behavior-preserving): collect events while locked, fan out after unlocking — Repairs Applied (all safe/additive per §7.4)
Repair Gate (§7.5)
Verdict
Rationale: binding gate is §2.7 generator provenance (single-actor pipeline); no axis at 🔴 after repairs. Generated by Claude Code |
Summary
Memory and several agent-loop signals were completely silent — no logs, no notifications, no programmatic surface — while the skills subsystem already had a full event pipeline (event → notifier → terminal/web/Telegram/programmatic). This PR brings memory and the loop up to parity using the same proven notifier pattern.
Memory lifecycle events (
internal/memory)notifier.go:MemoryEvent+MemoryNotifierinterface +Noop/Multiimplementations, mirroringskills/notifier.go.MemoryManager.SetNotifierpropagates to theEpisodeStoreso facts and episodes share one sink.fact_addedfact_mergedfact_replaced/fact_removedfact_consolidatedepisode_storedUntrusted)episode_dedupedepisode_evictedepisode_promotedepisode_pending_reviewAddFact's merge-on-write branch now marks the system prompt dirty, so a merged fact is visible on the next turn (was previously invisible until an unrelated mutation).Agent-loop signals (
internal/loop)signal.go:SignalEvent+SignalHandler+Engine.SetSignalHandler.context_trimmed(proactive budget trim and post-error survival trim) andtool_recovery(repeated tool failure → corrective hint) — both previously silent.Surfaces (all four)
render.Renderermemory/episode/signal methods, gated behind a newmemoryVerboseflag (enabled by verbose interaction mode).Config.MemoryEventHandler+Config.AgentSignalHandlerwith adapters that fan out alongside the renderer.serve.gostreamsmemory_event/agent_signalover the WebSocket;app.jsdispatches them to toasts (high-frequency events kept silent).Tests & docs
internal/memory/notifier_test.go(facts/episodes, dedup-is-silent, manager→episode propagation),internal/loop/signal_test.go, and render tests for the new methods.go vet ./...clean,gofmtclean, fullgo test ./...passes.docs/MEMORY.md(full event table) andAGENTS.mdsource layout.Note
The terminal surface is opt-in (verbose mode) to avoid flooding default output — matching the skills precedent. Web/Telegram/programmatic handlers receive events regardless. Flipping terminal logs on by default is a one-line gating change if preferred.
https://claude.ai/code/session_01L66aFQQ4SyniU7m5Ah3iTX
Generated by Claude Code