fix(embeddings): embed in the service, so every caller gets it (#2056)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 29s

A note or task created through MCP was not semantically searchable until the
next restart's backfill ran. Embedding fired at the five REST route handlers
and nowhere else; the MCP tools call the service directly, so they skipped it.

The shape of this bug is the reason to care: it is invisible on an instance
that redeploys constantly (this one does, per rule 46) and permanent on one
that doesn't. Rule 115 — the product has to stand up for the install that
restarts twice a year, not just for the one that restarts hourly.

Moved to services/notes.embed_note(), called from create_note and update_note,
and deleted from all five routes. Every caller — REST, MCP, recurrence,
snippets — now gets it by construction rather than by remembering.

Two things fall out of having one implementation instead of six:

- It uses note.user_id, the OWNER. The routes were inconsistent: some passed
  the caller's uid, some the owner's. On a shared record the caller's id mints
  a second embedding row that nothing reads.
- services/snippets.py's _embed_snippet existed only because snippets are
  created via MCP and the routes couldn't cover them. Every one of its four
  call sites goes through notes_svc, so the helper and its four calls are gone,
  along with the eight test patches that existed to neutralise it.

RuntimeError (no running loop — unit tests, scripts) and any indexing failure
are both swallowed: a write that succeeded must not be failed by its index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
This commit is contained in:
2026-07-31 22:46:57 -04:00
co-authored by Claude Opus 5
parent 4c9a637507
commit 5c51e29f26
8 changed files with 107 additions and 58 deletions
+3 -6
View File
@@ -39,8 +39,7 @@ async def _run_merge(target, sources, source_ids):
patch("scribe.services.access.can_write_note", AsyncMock(return_value=True)), \
patch.object(s.notes_svc, "update_note",
AsyncMock(return_value=target)) as mock_update, \
patch("scribe.services.trash.delete", AsyncMock(return_value=object())), \
patch.object(s, "_embed_snippet", MagicMock()):
patch("scribe.services.trash.delete", AsyncMock(return_value=object())):
await s.merge_snippets(7, target.id, source_ids)
return mock_update.await_args.kwargs
@@ -88,8 +87,7 @@ async def test_an_ordinary_edit_carries_provenance_forward():
with patch.object(s, "get_snippet", AsyncMock(return_value=note)), \
patch("scribe.services.access.can_write_note", AsyncMock(return_value=True)), \
patch.object(s.notes_svc, "update_note",
AsyncMock(return_value=note)) as mock_update, \
patch.object(s, "_embed_snippet", MagicMock()):
AsyncMock(return_value=note)) as mock_update:
await s.update_snippet(7, 1, signature="f(ms) -> string")
kwargs = mock_update.await_args.kwargs
@@ -106,8 +104,7 @@ async def test_a_pre_0070_row_keeps_provenance_through_the_body():
with patch.object(s, "get_snippet", AsyncMock(return_value=note)), \
patch("scribe.services.access.can_write_note", AsyncMock(return_value=True)), \
patch.object(s.notes_svc, "update_note",
AsyncMock(return_value=note)) as mock_update, \
patch.object(s, "_embed_snippet", MagicMock()):
AsyncMock(return_value=note)) as mock_update:
await s.update_snippet(7, 1, when_to_use="humanize a ms count")
assert "**Merged from:** #5" in mock_update.await_args.kwargs["body"]