Fix summarizer hot path: capture on SessionEnd, drain in background#8
Fix summarizer hot path: capture on SessionEnd, drain in background#8jamby77 wants to merge 10 commits into
Conversation
Every SessionSummarySchema field has a default, so {} parses into a
valid-looking husk. Drop those instead of storing them.
The hook duplicated AgingPipeline.processIngestQueue with worse error handling. It now only queues the transcript.
The capture hook now returns immediately and a background drain does the summarization, so no LLM work runs inside a Claude Code hook.
Stop fires each time Claude finishes responding, so the plugin summarized the whole session on every turn. Also removes the stale Stop registration from existing installs.
The hook registration was renamed to SessionEnd but the install summary still printed Stop.
storeMemory embeds buildEmbedText (the full summary); the recall helper re-embedded only oneLineSummary, so with the deterministic fake embed the stored and query vectors diverged past the 0.25 distance threshold and KNN returned nothing. Re-embed the same text so the round-trip is faithful.
Install and config already fall back to USERPROFILE; the drain spawn did not, so on Windows the path never resolved and the queue silently never drained.
register-hooks.ts registers hooks as `bun run <src>` and compiles no binaries, so ~/.betterdb/bin/drain does not exist on that path. The exists() guard then skipped the spawn silently and nothing ever drained the queue. Resolve the compiled binary first, the sibling source second, and report to stderr when neither is found.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort 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 2e3e752. Configure here.
| // Stop registration must be stripped explicitly or it survives upgrades. | ||
| const existingHooks = stripLegacyBetterdbHooks( | ||
| (settings["hooks"] ?? {}) as Record<string, unknown[]>, | ||
| ); |
There was a problem hiding this comment.
Install omits dev hook markers
Medium Severity
betterdb-memory install calls stripLegacyBetterdbHooks with only the default betterdb marker, while register-hooks.ts also passes the plugin src/hooks path. A leftover Stop registration whose command path does not contain betterdb can survive install and keep firing the old per-turn hook alongside SessionEnd.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 2e3e752. Configure here.
KIvanow
left a comment
There was a problem hiding this comment.
Approve. This fixes a real, costly bug — capture was on Stop (fires every turn), so the plugin re-summarized the whole session on every response and stored empty husks. The fix is sound: SessionEnd fires once, the hook does zero model work (queues transcript + spawns a detached unref()'d drainer), and isEmptySummary correctly distinguishes an all-defaults husk from a real summary — every SessionSummarySchema field has a .default(), so {} parses as valid and the predicate is the only thing that can tell them apart. Husks are dropped, not re-queued (no infinite loop). The stacked commits also show careful verification (Windows USERPROFILE fallback; binary-vs-source drain resolution so the dev path isn't silently skipped).
Verified locally: bun run typecheck clean · bun test tests/unit 164/164 green.
Two non-blocking nits:
src/hooks/drain.ts+src/index.tsrunDrain()— these are near-identical copies of the same pipeline wiring (client + model + store +processIngestQueue). Worth collapsing into one shared helper so the two don't drift.src/index.ts(formatting) — a large share of the +676 is Prettier reflow of untouched code, which makes the meaningful change harder to isolate in review. Not wrong, just flagging that bundling a formatting pass with a behavior change inflates the diff.
Pre-existing bug worth a fast-follow (not introduced here, but #9 raises its blast radius):
src/memory/aging.tsprocessIngestQueue(catch →break) —popIngestQueue(20)pulls up to 20 items into memory; on the first summarizer failure only the failing item is re-queued and the loopbreaks, silently dropping every already-popped item after it. Once #9's segmented capture lands, a long session enqueues many segments, so one transient Ollama hiccup mid-drain discards the rest of that session. (The re-queue alsorpushes to the tail, reordering segments — cosmetic, since each becomes an independent memory.) Suggest re-queuing the remaining unpopped items on failure, or switching to pop-one-process-one.


Problem
The
Stophook fires on every turn, so the plugin re-summarized the whole session on each response — burning minutes of local LLM time on a hook, blocking the turn, and discarding the work when the Ollama client timed out. The success path stored empty husks (SessionSummarySchema.parse({})passes and yields "Session recorded — summary unavailable"). Net result over 8 days: one memory, and it was empty; after a GPU fix, near-duplicate memories appeared one-per-turn.Change
SessionEnd(fires once), notStop(every turn) — with a migration that strips the staleStopregistration from existing installs.isEmptySummarypredicate); husks are dropped, not re-queued.Stoplabel in the install summary, fix an integration test that recalled by the wrong embed text, and gitignore.idea/.Verification
bun run typecheckclean ·bun test tests/unitgreen ·bun test tests/integrationgreen (against Valkey on 6390).Stack
Bottom of a 2-PR stack. The follow-on #9 (checkpoint capture) builds directly on this branch — merge this one first.
Note
Medium Risk
Changes when and how session memories are captured (hook lifecycle + async drain); misconfiguration could leave transcripts queued until drain runs, but hooks no longer block on LLM calls.
Overview
Moves session memory capture off the per-turn
Stophook toSessionEnd, so summarization no longer runs on every assistant response or blocks the hook path.The
session-endhook now only parses the transcript, pushes to the Valkey ingest queue, and spawns a detacheddrainprocess (compiled binary orbun runsource).AgingPipeline.processIngestQueueperforms LLM summarization and storage. Install andregister-hooksregisterSessionEndinstead ofStop, andstripLegacyBetterdbHooksremoves staleStopentries on upgrade without touching third-party hooks.Adds
isEmptySummaryso default-schema “husks” are dropped from the ingest queue (not stored or re-queued). Newbetterdb-memory draincommand anddrainbinary in the install set. Hook payload schema switches fromStopPayloadtoSessionEndPayload.Reviewed by Cursor Bugbot for commit 2e3e752. Bugbot is set up for automated code reviews on this repo. Configure here.