Skip to content

fix: make proactive recall reliably deliver value (5 root-cause fixes)#287

Merged
khustup2 merged 6 commits into
feat/proactive-recallfrom
fix/hivemind-summary-revert-race
Jun 25, 2026
Merged

fix: make proactive recall reliably deliver value (5 root-cause fixes)#287
khustup2 merged 6 commits into
feat/proactive-recallfrom
fix/hivemind-summary-revert-race

Conversation

@khustup2

Copy link
Copy Markdown
Contributor

Make proactive recall actually deliver value (5 fixes)

Built an isolated-Docker A/B harness that measures whether Hivemind's
cross-session memory changes a second session's answer. It surfaced that recall
rarely fires in practice, and production confirms it: of 4,732 real
session summaries, only 11.5% are recall-eligible — 56% stuck
description='in progress'
(never finalized) and only 25% embedded. This
PR fixes the five root causes. After them, the A/B value-win rate goes 1/9 →
5/9
, "recall fired but answered wrong" goes 80% → 0%, and recall firing on
a cross-person demo goes 1/3 → 3/3.

Stacked on feat/proactive-recall (the diff is exactly these 5 commits).

The fixes

  1. recall fidelity (recall-format/gate/query.ts, spawn-wiki-worker.ts) —
    inject a verbatim ## Key Facts excerpt (not just the gist description);
    preserve exact identifiers in the summary prompt; threshold 0.55→0.50
    (env-tunable). Kills the "recall fired but dropped the exact fact" failure.
  2. finalize-wins (upload-summary.ts) — a placeholder/empty write can no
    longer downgrade a finalized summary back to a stub.
  3. embedding coverage (embed-summary.ts, wiki-worker.ts) — warm the embed
    daemon + retry on finalize so a cold daemon no longer strands
    summary_embedding = NULL forever (there was no backfill).
  4. recall warm-daemon (recall.ts) — warm the daemon + retry the query
    embed so the first prompt of a session doesn't miss semantic recall
    (cold-daemon race); budget 1000→1500ms, still under the 2s hook timeout.
  5. summary revert race (shared/placeholder-summary.ts + 4 session-start
    variants) — the root blocker: createPlaceholder did SELECT-then-INSERT
    and the memory table has no unique constraint on path, so a stale read on
    a second SessionStart inserted a duplicate in progress stub that shadowed
    the finalized row (unordered LIMIT 1 reads). Replaced with an atomic
    INSERT … SELECT … WHERE NOT EXISTS. This is the mechanism behind the 56%
    stuck-in-progress production stat.

Tests

Each fix is red→green with unit tests (vitest): finalize-wins, embed-summary
warmup/retry, recall fidelity/threshold, placeholder revert-race (incl. the
stale-read duplicate case), and an updated EmbedClient mock. Target suites green;
remaining whole-suite failures are pre-existing env issues (tree-sitter native
addons, network smoke tests) unrelated to these changes.

Validation

Measured against the live backend in isolated containers (one per claude
session; continuity only through Deeplake). Reproducible harness + before/after
demo cards available separately.

🤖 Generated with Claude Code

khustup2 added 6 commits June 24, 2026 05:53
…shold

RECALL_LOSSY: the injected hint carried only the gist 'description'
(derived from ## What Happened), which by design drops exact identifiers
/values. Now select the full 'summary' and inject an excerpt built from
the high-signal fact sections (## Key Facts / ## Decisions / ## Entities)
that hold verbatim, non-derivable values; description is the fallback for
legacy rows. Strengthen WIKI_PROMPT_TEMPLATE so What Happened + Key Facts
preserve exact values verbatim.

RECALL_NOT_FIRED: lower the semantic cosine bar 0.55 -> 0.50 (query vs
long document embeddings routinely land in 0.50-0.55 even on clear hits),
operator-tunable via HIVEMIND_RECALL_THRESHOLD (bounded 0<t<=1).

Backward-compatible: summary is optional on RecallHit; lexical path
already searched summary. Tests: +12 recall cases, full claude-code +
recall suites green (2822).
…mary

A finalized summary row (real summary body + non-placeholder description)
must never be overwritten by a SessionStart placeholder or a content-free
stub. In production ~56% of summaries on the activeloop memory table were
stuck at description='in progress', invisible to proactive recall, because
uploadSummary did a blind UPDATE-by-path: a stale/duplicate writer (resumed
session, or a late wiki worker that produced empty/content-free text) could
downgrade an already-finalized row back to a stub.

uploadSummary now fetches the existing summary+description and skips the
write when the incoming text is not a real summary (no populated
'## What Happened' section) AND the existing row is already finalized.
extractDescription's 'completed' fallback is not a reliable finalized
signal, so the guard keys on the populated section instead.

Backward compatible: INSERT path and finalized->finalized refresh
(resumed sessions) are unchanged; only stub->finalized downgrades are
blocked.
…dding coverage

Production memory table: only 25% of summaries carry a summary_embedding,
so proactive recall silently degrades to weak lexical matching for ~75% of
the corpus. A large share of those NULLs are ENABLED users whose embed
daemon was cold at the single moment the wiki-worker finalized the summary.

The wiki-worker called EmbedClient.embed() directly. That call is tuned for
the latency-critical capture hook: on a cold daemon it returns null
immediately while spawning in the background (fire-and-forget). Since no
backfill ever revisits a NULL row, one cold-start race stranded the summary
in lexical-only mode permanently.

The wiki-worker is a detached background process, not latency-critical, so
it can afford to warm the daemon and retry. Add embedSummaryWithWarmup()
(warmup -> embed -> one retry, never throws) and use it in the claude-code
wiki-worker. No change to the hot capture path or the opt-in semantics
(callers still gate on embeddingsDisabled()); graceful NULL fallback when
the daemon truly cannot be reached.
… recycle race); update EmbedClient test mock
… finalized summaries to 'in progress' stubs

Root cause of the production clobber (~56% of summaries stuck at 'in
progress', ~75% with NULL embeddings): the memory table has NO unique
constraint on `path` (deeplake-schema.ts MEMORY_COLUMNS). createPlaceholder
used a SELECT-then-INSERT guard:

    SELECT path ... WHERE path = $p LIMIT 1
    if (rows.length === 0) INSERT placeholder

This is a TOCTOU race. Deeplake reads are eventually-consistent, so a
second SessionStart for the SAME session id (a --resume, a source:
resume|clear re-fire, or two near-simultaneous SessionStart invocations)
can read ZERO rows even though the wiki worker finalized + embedded the row
seconds earlier. The guard passes and a SECOND, stub placeholder row is
INSERTed at the same path. Downstream reads use `... WHERE path=$p LIMIT 1`
with no ORDER BY, so the duplicate `description='in progress',
summary_embedding=NULL` stub shadows the finalized row — recall silently
drops it. This path bypasses uploadSummary's finalize-wins guard entirely.

Fix: extract the placeholder write into a shared helper
(src/hooks/shared/placeholder-summary.ts) used by all four agent variants
(claude-code, codex, cursor, hermes). The write is now a single atomic,
finalize-aware statement:

    INSERT INTO t (...) SELECT <values>
      WHERE NOT EXISTS (SELECT 1 FROM t WHERE path = $p)

The existence check and insert happen in one server-side statement, so no
stale client-side read can wedge a duplicate in between. If ANY row already
exists at the path (placeholder OR finalized), the INSERT writes nothing — a
finalized row can never be reverted to a stub or have its embedding nulled
by a placeholder write. The fast-path SELECT is kept as a logging/skip
optimization but is no longer the safety boundary; a failed SELECT no longer
blocks the (race-safe) write.

Also de-duplicates four near-identical createPlaceholder copies.

Regression test (tests/claude-code/placeholder-summary.test.ts): a
finalized+embedded row survives a subsequent placeholder/SessionStart write,
a STALE-READ SessionStart, and a second concurrent worker pass. Red before
(plain VALUES insert appends a duplicate stub), green after.
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • main

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 89a712bd-9fa3-473d-bf38-53feadd3fae6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/hivemind-summary-revert-race

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@khustup2
khustup2 merged commit c44c40b into feat/proactive-recall Jun 25, 2026
7 of 8 checks passed
@khustup2

Copy link
Copy Markdown
Contributor Author

Superseded by #288. The root cause of unreliable recall was a pg-deeplake read-consistency bug (in-place UPDATE'd summary rows reading back stale), fixed separately on the backend. With that fixed, #288 carries the minimal, non-embedding improvements that help every path including the no-embeddings (lexical) default — finalize-wins, atomic placeholder, and recall content fidelity. The embedding-specific changes here (recall warm-daemon, embedding-coverage warm+retry, model-swap) are deferred to separate branches pending a decision to prioritize the semantic path. Closing this in favor of #288.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant