Repository navigation
Memory: make memory_update work without an embedding provider and keep category links - #485
Conversation
…p category links memU's update_memory_item step embeds new content unconditionally. With no OpenAI key, Nerve configures no "embedding" profile, memU aliases it to the "default" chat profile, and the OpenAI-SDK client then POSTs /embeddings to the Anthropic API, which answers 404. Every content update through the memory_update tool and the web UI failed with "Failed to update memory". The same step reads categories=None as "no categories" and unlinks the item from all of them. With the 404 gone, a content-only update would therefore silently drop every category link the item has. Nerve now replaces that step in memU's patch_update pipeline with its own. The new step embeds only when an embedding provider is configured, keeps the links when no categories are passed, and skips the re-embed and the category-summary rewrite when the content is unchanged. The last part matters because the web UI resends the full text on every save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| if content == old_content: | ||
| # The web UI resends the full text with every edit, so an | ||
| # unchanged summary must not trigger a re-embed or a category | ||
| # summary rewrite. | ||
| content = None |
There was a problem hiding this comment.
[P2] Preserve unfinished category-summary work on retry
The item repository commits the new text before persist_index calls the LLM to patch category summaries. If that call times out, update_item() reports failure even though the item text is already saved. On an identical retry, this equality check sets content = None, so the later if content: block produces no category updates and the operation reports success with the old category summary still persisted.
Reproduced through full bridge initialization and the public update_item() method with a real SQLite store and one injected category-LLM timeout: the first call returns False with new item text / old category summary; the retry returns True, makes zero summary calls, and leaves that mismatch intact. This occurs with and without embeddings. The original memU step makes another summary call on retry. The partial commit predates this PR; suppressing recovery on retry is introduced here.
Please keep category-summary recovery independent of the embedding no-op optimization: only skip summary work when its completion is known, or defer that optimization. A failure-then-identical-retry regression test would cover this.
Problem to be solved:
memory_updatefails on every content update when no OpenAI key is configured. The tool only reportsFailed to update memory <id>.The log shows the cause:memU's
update_memory_itemstep embeds new content unconditionally. Without an OpenAI key, Nerve configures noembeddingprofile, and memU's settings alias it todefault(memu/app/settings.py). The OpenAI-SDK client then POSTs/embeddingsto the Anthropic API, which answers 404. On Bedrock the call goes to the placeholder base URL instead. Nerve already works around this formemorizeby replacingcategorize_items, but it doesn't for updates. Deletes don't embed, which is why they kept working. Edits from the web UI (PATCH /api/memory/memu/items/{id}) go through the same path.A second bug sits behind the first. The same step reads
categories=Noneas "no categories" and unlinks the item from every category it had. In_patch_update_memory_item,cats_to_removeis the old set minusmap(None) == []. The agent tool passesNonewhenever no categories are given. So once the 404 is gone, every content-only update would silently drop the item's category links and tell each category summary that the content was discarded. This also hits instances that do have an OpenAI key today.Changes:
MemUBridge._update_item_stepreplaces memU'supdate_memory_itemstep in thepatch_updatepipeline. It keeps the same step contract (requires,produces,capabilities,config), so memU's persist and response steps run unchanged. The new step:embeddingprofile;categoriesisNone, while an explicit list (including[]) relinks exactly as before;MemoryServiceis constructed, before the bridge marks memU as available. If installing fails, memU is reported as unavailable instead of half-initialized.categorize_itemsswap. A missingembeddingprofile doesn't raiseKeyError; it falls back to the chat profile.Testing:
TestUpdateItemStepruns memU's realpatch_updatepipeline against a real SQLite store: its step validation, LLM-client resolution, persist step and response step. Only the per-profile base clients are fakes. It covers:embeddingprofile when a provider is configured;defaultprofile, always embedding, dropping theNonehandling, and dropping the unchanged-content check._memu_repo()now delegates to a new_memu_db()helper that returns the whole shared database, because the new tests also need the category repos.Known gaps, left for follow-ups:
_map_category_names_to_idssilently drops names it doesn't know, somemory_update(categories="typo")still unlinks everything and reports success. Rejecting unmapped names seems right, but it changes behavior, so it belongs in a separate PR.embedding=Nonemeans "unchanged" in memU's repo. This is harmless until a key is added back.memorizealready surfaceslast_error, andmemory_updatecould do the same.🤖 Generated with Claude Code