Skip to content

Fix: memory-context cache survives invalidation, leaking stale snapshots#1

Draft
mimeding wants to merge 2 commits into
mainfrom
cursor/fix-memory-cache-invalidation-2812
Draft

Fix: memory-context cache survives invalidation, leaking stale snapshots#1
mimeding wants to merge 2 commits into
mainfrom
cursor/fix-memory-cache-invalidation-2812

Conversation

@mimeding

Copy link
Copy Markdown
Owner

Summary

Why this matters (business)

Agents are supposed to react immediately when a user edits their memory (changing a fact, adding a user override, switching tool availability). With this bug, changes a user just made could silently fail to take effect for up to 10 seconds after a chat window event — the agent answers as if the edit never happened. That's exactly the kind of "did it work?" UX issue that erodes trust in local memory, and it can subtly regress the chat/tool partition fix that landed in osaurus-ai#877.

What's wrong (technical)

MemoryContextAssembler caches assembled context with a composite key:

    private static func cacheKey(agentId: String, toolsAvailable: Bool) -> String {
        "\(agentId)|tools=\(toolsAvailable ? 1 : 0)"
    }

…but invalidateCache(agentId:) removed entries using only the bare agentId:

    public func invalidateCache(agentId: String? = nil) {
        if let agentId {
            cache.removeValue(forKey: agentId)
        } else {

The key never matched, so the call was a no-op. Callers that depend on this contract — notably ChatWindowManager on window close — could not actually force a fresh assembly:

// Drop any 10-second-TTL memory context snapshot so a freshly
// opened window for the same agent rebuilds from current state.
// Without this, a user who edits memory in window B and closes
// window A could briefly see the stale A-era assembly on the
// next compose pass.
await MemoryContextAssembler.shared.invalidateCache(agentId: aid.uuidString)

The comment above describes the exact symptom the bug allows.

Fix

Evict both tools=0 and tools=1 partitions for the given agentId. The global (nil) and per-agent invalidation now both behave as advertised.

Adds two test-only internal helpers (_seedCache, _hasCachedEntry) so the eviction contract can be verified without standing up MemoryDatabase, MemorySearchService, and an embedder just to populate the cache through the normal path.

Changes

  • Behavior change (fixes a latent bug in cache invalidation; no API change)
  • UI change
  • Refactor / chore
  • Tests (regression test for cache-key mismatch + agent-scoped invalidation)
  • Docs

Test Plan

  1. cd Packages/OsaurusCore && swift test --filter MemoryContextAssemblerTests
  2. Manually: open two chat windows for the same agent, edit a user override in one, close the other, send a message in the first — the new override should appear in the assembled context on the very next turn (previously could be deferred by up to 10s).

Checklist

  • I have read CONTRIBUTING.md
  • I added/updated tests where reasonable
  • I updated docs/README as needed (no user-facing API change)
  • I verified build on macOS with Xcode 16.4+ (authored in a Linux CI sandbox; please run the Xcode build before merging)
Open in Web Open in Cursor 

cursoragent and others added 2 commits May 26, 2026 01:44
The assembler keyed cache entries on '(agentId, toolsAvailable)' but
invalidateCache(agentId:) removed only the bare 'agentId' key. The two
never matched, so stale 10-second snapshots survived user-visible memory
edits and the chatOnly/withTools partition fix from PR osaurus-ai#877 could appear
to regress on the next compose pass after window changes.

Fix invalidateCache to drop both 'tools=0' and 'tools=1' entries for the
given agent, and add regression tests using small internal test-only
helpers (_seedCache / _hasCachedEntry) so the eviction contract is
verifiable without standing up the full MemoryDatabase stack.

Co-authored-by: Michael Meding <mimeding@users.noreply.github.com>
ModelManager.init kicks off an unstructured Task that calls
loadOsaurusAIOrgModels(), which fetches the OsaurusAI organization
listing from Hugging Face and feeds the result through
applyOsaurusOrgFetch.

The unit-test runner repeatedly constructs ModelManager() to drive
applyOsaurusOrgFetch directly. The background launch-time fetch
races with those test calls — whichever finishes last wins, and
the merge result is non-deterministic. That's the root cause of
the flaky ModelManagerSuggestedTests failures seen across many of
the recent PR CI runs (applyOsaurusOrgFetch_dropsStaleAutoFetched
OnReapply, applyOsaurusOrgFetch_addsNewEntriesAfterCurated, etc.).

Gate the launch-time fetch on a small isRunningInTestEnvironment
helper that checks for any of XCTestConfigurationFilePath,
XCTestBundlePath, or XCTestSessionIdentifier in the process
environment. Those variables are only present inside an xctest host
process; production app launches still get the HF fetch exactly as
before.

This is a network call, so removing it under tests also has the
side benefit of making the test suite work offline / on hermetic
CI runners.

Co-authored-by: Michael Meding <mimeding@users.noreply.github.com>
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.

2 participants