Skip to content

fix(code-intelligence): a failed embedding cache write no longer discards the embedding (#13437) - #13495

Merged
mrveiss merged 2 commits into
Dev_new_guifrom
issue-13437
Aug 4, 2026
Merged

mrveiss merged 2 commits into
Dev_new_guifrom
issue-13437

Conversation

@mrveiss

@mrveiss mrveiss commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Closes #13437

Thinking Path

_generate_embedding wrapped generation and the cache write in one try/except Exception:

embedding = await generate_embedding_with_fallback(...)
if embedding:
    await self._embedding_cache.put(...)   # same try
    return embedding

A failing EmbeddingCache.put therefore discarded a perfectly good vector and fell through to the SHA-256 pseudo-embedding. What makes it costly is that it fails invisibly — a well-shaped 768-dimension vector still comes back, so nothing errors. Every pattern written while the cache misbehaved was indexed under a vector encoding nothing about the code, quietly degrading code_patterns similarity search.

The cache is an optimisation. Its failure must never change the value returned.

This is the third defect of this shape in this one function. #12374 and #12407 were both a broad except swallowing something and silently substituting the pseudo-embedding. The existing test file was written for #12407 — it covers generation thoroughly and the write not at all, which is exactly the gap this fell through.

What Changed

File Change
pattern_analysis/analyzer.py Generation and cache write guarded separately; _hash_pseudo_embedding extracted
pattern_analysis/embedding_test.py Three tests: cache-write failure, generation failure, generation returning nothing

A cache-write failure now logs and returns the real embedding. The hash fallback is reached only when generation itself fails or yields nothing — two of the new tests pin that, because this narrows the except, it does not remove it.

Function length. The change pushed _generate_embedding to 43 code lines against the repo's 30-line rule (it was 31 before). Extracting _hash_pseudo_embedding brings it to 34, with a 5-line helper — and lets the fallback's docstring say plainly that it encodes nothing semantic, which is the property that makes silently returning it harmful.

Verification

code_intelligence/pattern_analysis/embedding_test.py    10 passed  (was 7)
isort · black · flake8 (0)                              clean

Mutation-checked. Restoring the combined try fails the new test:

FAILED ...::TestCacheWriteFailureDoesNotDiscardTheEmbedding::test_returns_the_real_embedding_when_the_cache_write_fails

The two fallback tests pass under that mutation as well as after the fix, which is correct — they guard a path the change deliberately preserves.

Model Used

Claude Opus 5 (1M context)

mrveiss added 2 commits August 4, 2026 12:22
…ards the embedding (#13437)

_generate_embedding wrapped the embedding generation AND the cache write in one
try/except Exception:

    embedding = await generate_embedding_with_fallback(...)
    if embedding:
        await self._embedding_cache.put(...)   # same try
        return embedding

So a failing EmbeddingCache.put threw away a perfectly good vector and fell
through to the SHA-256 pseudo-embedding. Every pattern written while the cache
misbehaved was indexed under a vector that encodes nothing about the code,
silently degrading code_patterns similarity search -- the failure is invisible
because a well-shaped vector is still returned.

The cache is an optimisation. Its failure must never change the value returned,
so generation and the write are now guarded separately. A cache-write failure
logs and returns the real embedding; the hash fallback is reached only when
generation itself fails or yields nothing.

This is the third defect of this shape in this one function. #12374 and #12407
were both a broad except swallowing something and silently substituting the
pseudo-embedding; the existing test file was written for #12407 and covers
generation, not the write. Three new tests close that gap and also pin that the
fallback stays reachable -- this narrows the except, it does not remove it.

Extracted _hash_pseudo_embedding while here: the function had grown to 43 code
lines against the repo's 30-line rule (31 before this change). It is now 34 and
the helper is 5, and the fallback's docstring can state plainly that it encodes
nothing semantic.

Mutation-checked: restoring the combined try fails
test_returns_the_real_embedding_when_the_cache_write_fails. 10 passed.

Also reformatted a malformed "# nosec B110 - prose" annotation: bandit parses
everything after the test id as further ids and warns per word, which made the
no-severity-floor bandit run non-silent for anyone touching this file.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

@mrveiss
mrveiss merged commit 17f2c1b into Dev_new_gui Aug 4, 2026
48 of 60 checks passed
@mrveiss
mrveiss deleted the issue-13437 branch August 4, 2026 09:47
mrveiss added a commit that referenced this pull request Aug 4, 2026
* chore: claim worktree

* fix(lint): keep prose out of the nosec test-id list (#13521)

Bandit parses everything after the suppression token as a whitespace-separated
list of test IDs, so a trailing explanation becomes a list of bogus IDs and it
warns once per word. One line in context_window_manager.py produced three
warnings; slash_command_handler.py produced twenty.

That matters because code-quality.yml runs bandit with no severity floor, so
its output is expected to be silent, and scripts/pr-preflight.sh treats a
non-empty bandit run as a failure. It was a papercut that recurred: it fired in
three separate PRs on one day -- #13438, #13434 and #13495 -- each fixed a
single file at a time by whoever happened to be editing it.

390 annotations across 191 files are rewritten from

    },  # nosec B105 - config dict; token_estimation is not a credential

to

    },  # nosec B105  # config dict; token_estimation is not a credential

The second hash ends bandit's parse, so the explanation survives and the
annotation is silent. Verified rather than assumed before applying: the old
form emits 6 warnings on a one-line file, this form and the preceding-comment
form both emit 0, and both still suppress.

Four separator variants were present (-, en dash, em dash, colon); all are
handled. One prose comment in api/database_mcp.py also had to be reworded --
it discussed the token in running text, which bandit cannot distinguish from a
real annotation.

scripts/check_nosec_format.py enforces the format, wired into code-quality.yml
beside the other repo-invariant checks. Its own tests assemble the token at
runtime rather than spelling it out, because bandit scans test files too and
literal fixtures would have made the checker's tests the last violation.

Also worth recording: the old form is not as narrowly scoped as its authors
intended. "# nosec B105 - explanation" discards the unrecognised extras, so it
reads as scoped while behaving as a bare "# nosec B105".

Verification:
  malformed annotations remaining            0
  bandit "Test in comment" warnings          0 across autobot-backend,
                                             autobot_shared, autobot-slm-backend,
                                             scripts, tools
  suppressions still effective               identical issue counts on a
                                             5-file sample, base vs branch
  194 rewritten files parsed with ast        0 syntax errors
  430 annotations, per-file id multisets     0 files changed their id set
  a2a + utils + api + repo_tests + scripts   2414 passed
  checker tests                              13 passed

Two things the rewrite exposed, both handled:

The longer annotation pushed three lines past 120 characters, black reflowed
them, and the suppression landed on the closing-bracket line -- orphaned from
the expression it protected. That is the #9489 hazard database_mcp.py already
documents. Those three now put the prose on its own line above the statement,
where black cannot move it. Verified: orphan count is 38 on base and 38 on the
branch, zero files made worse.

Investigating those three showed 38 suppressions were ALREADY orphaned on base
and suppressing nothing. Filed as #13528 rather than fixed here -- clearing the
390 warnings is what made them visible.

Also narrows pr-preflight.sh's bandit gate: "nosec encountered, but no failed
test" is bandit reporting an unnecessary suppression in a file you happened to
touch, not a defect in your change. Blocking on it would stop anyone editing a
file that carries a stale suppression.
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