Skip to content

fix(sdk/recall): harden supersede counters, slot key contract, and resolver path#49

Merged
lIang70 merged 1 commit into
mainfrom
fix/recall-supersede-resolver-hardening
Apr 29, 2026
Merged

fix(sdk/recall): harden supersede counters, slot key contract, and resolver path#49
lIang70 merged 1 commit into
mainfrom
fix/recall-supersede-resolver-hardening

Conversation

@lIang70

@lIang70 lIang70 commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Hardens the slot supersede + LLM update resolver feature in sdk/recall after a code review that surfaced counter accounting bugs, a missing escape hatch, and several maintainability issues.

Bugs (correctness)

  • B1supersedeBySlot only increments supersede_total{channel="slot"} after a successful Upsert, matching the vector / resolver channels. Previously the metric was bumped by len(resp.Items) before the loop's skip-list ran, over-reporting in dashboards.
  • B2normalizeSubject lowercases unknown tokens so case-only differences ("Alice" vs "alice") collapse onto the same slot_key. The behaviour now mirrors normalizePredicate and is documented in godoc.
  • B3 — Add Request.WithTombstoned so callers can surface tombstoned entries (debug resolver, stage Auditable.Rollback). MetaTombstone is now documented as a reserved metadata key.

Design / maintainability

  • M1 / N6 — Centralise reserved metadata keys: new sdk/recall/metadata.go (MetaSubject, MetaPredicate, MetaSupersededBy, MetaSupersededAt, MetaTombstone, MetaContentHash, MetaSourceLabel, MetaEntities) plus sdk/retrieval/metadata.go (MetaSlotKey, SlotKeyOf). Pipeline's SlotCollapse and the recall package now share one lookup helper instead of duplicating it.
  • M2slotEligible(fact) rejects subject/predicate values containing the slot delimiter | so ambiguous slot_keys are never written; such facts gracefully degrade to the vector / resolver channels.
  • M3 — New WithoutSlotChannel() option separates the deterministic slot path from the noisy vector soft-merge. WithoutSoftMerge() now affects the vector channel only and the godoc spells the difference out.
  • M4 — Resolver invocation opens its own span memory.recall.resolver_batch with n_facts / n_candidates attributes for cost attribution without enabling SearchDebug.
  • M5WithUpdateResolver godoc explains the ordering relationship with the slot/vector supersede channels and the resulting candidate-count behaviour.

Polish

  • N1 — Drop misleading "future-proofs" comment in save.go; replaced with a "single source of truth" rationale that actually matches the code.
  • N2ResolveAction godoc clarifies that SourceID / TargetID are required for UPDATE / DELETE and informational/optional for ADD / NOOP.
  • N4 — Expand PredicateAliases coverage for high-frequency CN/EN surface forms across all v1 predicates (/住所/老家/故乡/供职/就职/出生/爱人/伴侣, based_in/married_to/born_on/son/daughter, …); table is now documented as curated rather than exhaustive, with WithPredicateAlias overrides retained as the per-namespace extension point.
  • N5LLMUpdateResolver sends a strict resolverActionsSchema alongside WithJSONMode so providers that enforce JSON-schema (Azure OpenAI strict) accept the call.
  • N7 — Drop now-stale references to docs/recall-update-resolution.md (file does not exist in tree); link to in-code symbols instead.

New tests

Test File Covers
TestTombstoneFilter_HidesByDefault_OptInReveals memory_test.go B3
TestSlotChannel_KeptWhenWithoutSoftMerge merger_test.go M3
TestSlotChannel_DisabledByWithoutSlotChannel merger_test.go M3 (new option)
TestSlotSupersede_MultipleStaleEntriesInOneSlot merger_test.go N3 (multiple stale entries)
TestSlotEligibility_RejectsDelimiterInSubject merger_test.go M2
TestSlotCollapse_EndToEnd_OnLegacyData merger_test.go N3 (end-to-end)
TestNormalizeSubject_LowercasesUnknownTokens aliases_test.go B2

Test plan

  • go build ./sdk/...
  • go vet ./sdk/...
  • go test ./sdk/recall/...
  • go test ./sdk/... ./sdkx/... ./voice/...

Made with Cursor

…solver path

Addresses a review of the recall package's slot supersede + LLM update
resolver feature. Changes are correctness, escape-hatch, and
maintainability fixes; no public API removals.

Bugs:
- supersedeBySlot only increments supersede_total{channel="slot"}
  after a successful Upsert (was over-counting candidates the loop
  later skipped).
- normalizeSubject lowercases unknown tokens so case-only differences
  ("Alice" vs "alice") collapse onto the same slot_key, matching
  normalizePredicate's behaviour.
- Add Request.WithTombstoned escape hatch and document MetaTombstone
  as a reserved key so pre-existing user data under "tombstone" can
  still be surfaced via Recall when needed.

Design:
- New WithoutSlotChannel option separates the deterministic slot path
  from the noisy vector soft-merge so callers can disable one without
  the other; WithoutSoftMerge now affects the vector channel only.
- slotEligible() rejects subject/predicate values containing the
  slot delimiter "|" so ambiguous slot_keys are never written; such
  facts gracefully degrade to the vector / resolver channels.
- Resolver invocation now opens its own span (memory.recall.resolver_batch)
  with n_facts / n_candidates attributes for cost attribution.
- Centralise reserved metadata keys under sdk/recall/metadata.go and
  sdk/retrieval/metadata.go; the SlotCollapse stage and recall package
  now share retrieval.SlotKeyOf instead of duplicating the lookup.

Polish:
- LLMUpdateResolver sends a strict resolverActionsSchema alongside
  WithJSONMode so providers that enforce JSON-schema (e.g. Azure
  OpenAI strict) accept the call.
- Expand PredicateAliases coverage for high-frequency CN/EN surface
  forms (lives_in / works_at / occupation / birthday / language /
  spouse / child / parent / pet) and document the table as curated
  rather than exhaustive; per-namespace overrides via
  WithPredicateAlias still win.
- ResolveAction godoc clarifies that SourceID/TargetID are
  informational/optional for ADD/NOOP and required for UPDATE/DELETE.
- Drop the now-stale references to docs/recall-update-resolution.md
  (file does not exist in tree); link to in-code symbols instead.

Tests:
- TestTombstoneFilter_HidesByDefault_OptInReveals (memory_test.go)
- TestSlotChannel_KeptWhenWithoutSoftMerge,
  TestSlotChannel_DisabledByWithoutSlotChannel,
  TestSlotSupersede_MultipleStaleEntriesInOneSlot,
  TestSlotEligibility_RejectsDelimiterInSubject,
  TestSlotCollapse_EndToEnd_OnLegacyData (merger_test.go)
- TestNormalizeSubject_LowercasesUnknownTokens (aliases_test.go)

Made-with: Cursor
@lIang70
lIang70 merged commit 2964401 into main Apr 29, 2026
9 checks passed
@lIang70
lIang70 deleted the fix/recall-supersede-resolver-hardening branch April 29, 2026 09:12
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