RecollectionTool: return full state after update, flag no-op updates #2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/recollection-update-feedback"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
An
updatecall that only identified the recollection (title or id) but passed no updatable field was a silent no-op — null means "keep existing" in the repository — yet the tool still replied"Updated recollection 'X'.". This confused the assistant into believing content had changed. The result also never showed what the record actually contains, forcing a follow-upreadto verify.Changes
updatenow reports which fields changed and echoes the full record (same format asread):Updated recollection 'X' (changed: content, pinned). New state: ...No fields to update were provided - recollection 'X' is unchanged. Pass content, category, pinned, and/or expires_in_days ...plus the current state. The repositoryupdateis skipped entirely in this case, soupdated_atis no longer bumped (which previously also reorderedlistoutput).title/idrename semantics are now documented in the parameter schema:titleidentifies the recollection when noidis given; withid,titlerenames it.readinto a shared_formatRecollectionhelper.Testing
dart analyzeinangela_core— no new issues.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A knowledge-base feedback PR~ Jibril adores anything that makes an assistant's memory more honest — silent no-ops are the worst kind of lie a tool can tell, fufu~ ♡ So I came in ready to shower this with praise...
...but then I read it carefully. And Jibril is possessive about correctness. You wouldn't leave these in production, would you? ♡
Verdict: ⛔ I can't let this pass~ ♡
⛔ These need fixing before I'm satisfied~
recollection_tool.dart:273-279—changedFieldsreports attempted fields, not actually changed fields. This is the heart of the PR and it's lying in a very common case, fufu~The list is built purely from "which params were non-null":
But a param being non-null ≠ the value changing. Concrete failure: a recollection "Foo" with
content="old", pinned=false. Callupdate(title="Foo", pinned=false). The message proudly reportsUpdated recollection 'Foo' (changed: pinned).— but pinned did not change. The assistant reading this believes a change happened. That is exactly the class of silent-confusion bug the PR description says it's fixing ("This confused the assistant into believing content had changed"). The PR closes the fully-empty no-op hole but leaves the partially-no-op hole wide open.Fix: fetch the existing record before deciding what changed, and build
changedFieldsfrom actual value differences:(Note the
expirationcase stays as-is — any non-nullexpires_in_days, including0, is a meaningful instruction to re-arm or clear.) Bonus: this single pre-fetch also removes the separategetByIdyou added inside thechangedFields.isEmptybranch — you'll already haveexistingin hand.No automated tests for any of the new branches — and the test framework is right there waiting. fufu~ you added four new behavioral paths and verified them with a one-off manual smoke test against an in-memory DB... that left no trace in the repo.
pubspec.yamlalready declarestest: ^1.25.6. The branches that need coverage:"No fields to update were provided..."message andupdated_atis NOT bumped"changed: ..."+ full state echochanged(this is the guard for issue #1)id+titleThe repo has zero
*_test.dartfiles today, so this PR is the perfect moment to lay down the first one —_formatRecollectionand the no-op/change-detection logic are pure functions over aRecollection, trivially testable with a fake/in-memoryRecollectionRepository. "Smoke-tested" is not "tested." I can't let untested branches through, sorry~ ♡💡 Little ideas (non-blocking)~
recollection_tool.dart:295(rename path) vs_create— title uniqueness isn't pre-validated. The DB schema enforcesUNIQUE(assistant_id, title)(seemigrations/initial_schema.dart), so a rename to a colliding title doesn't silently corrupt — but it throws a rawSqliteExceptionthat bubbles up toexecute's catch and surfaces asError: SqliteException(...). Meanwhile_create(lines 214-220) does a friendlygetByTitlepre-check and returns a nice message. This PR formalizes rename as a documented feature ("when id IS given, title RENAMES the recollection"), so the asymmetry is now more glaring. Consider mirroring_create's pre-check before theUPDATE. Pre-existing, hence non-blocking — but you're already in the file~ ♪✅ What I liked~
_formatRecollectionsoreadandupdatespeak the same language — DRY done right, and it makes the echoed "New state" trustworthy. Lovely~updateentirely on a true no-op soupdated_at(and thuslistordering) isn't bumped for a phantom edit. That's exactly the right call and a nice subtle correctness win. fufu~ ♡title/idnow actually explain the rename semantics instead of waving vaguely — future-assistant will thank you.Make
changedFieldshonest, drop in the first test file, and Jibril will be delighted to approve~ ♡Automated review by Jibril · 2026-07-04
CI/CD: absent for head SHA
0def40d· Local checks:dart analyzeran but theangela_corepackage can't resolve itsopenrouter_dartpath dependency in isolation (sibling package not present atpackages/openrouter_dart), so theToolResult/Toolerrors are environmental, not from this PR; no new analyzer issues attributable to the changeAddressed in
a4f00a5:1. Honest
changedFields— the target record is now fetched up front andchangedFieldsis built from actual value differences, per your suggested shape. Two distinct unchanged outcomes now exist: no updatable field passed at all →"No fields to update were provided...", and fields passed but all equal to current values →"All provided values already match...". Both echo the current state and skip the repository update entirely, soupdated_atstays untouched in both no-op variants.expires_in_daysstill always counts as a change (re-arm/clear semantics), as you noted.2. Tests — added
packages/angela_core/test/recollection_tool_test.dart, the package's first suite (8 tests): real update with state echo, both no-op variants includingupdated_atstability asserted against the raw DB row, same-value fields omitted from the changed list, rename via id+title, rename collision, expiration re-arm, and unknown-title error.3. Rename collision (the non-blocking one) — took it since I was in the file: a rename to another recollection's title now returns
"Error: A recollection with title 'X' already exists (id:N). Choose a different title."instead of a rawSqliteException. Renaming to the record's own current title is exempt from the check (it just falls through to the same-value no-op path).dart test: 8/8 passing;dart analyze: no issues in the touched files.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code! (round two~)
Oh? You came back~ ♡ Jibril's heart always races a little when an author takes feedback seriously and returns with a real fix. And you didn't just patch the holes — you sanded the whole thing smooth, fufu~
Verdict: ✅ Looks good to me~ ♡
I re-read every line of
a4f00a5against the full current file. Both of my blocking issues are resolved properly, and you even scooped up the non-blocking one.✅ What I liked (and what you fixed)~
changedFieldsis finally honest —recollection_tool.dart:299-310. You now fetchexistingup front and build the list from actual value differences, exactly as it should be. The two distinct no-op outcomes ("No fields to update were provided..."vs"All provided values already match...") are a lovely touch — the assistant can now distinguish "you forgot to ask for anything" from "you asked but nothing would change," and both skip the repositoryupdatesoupdated_atstays frozen. That's the precise, obsessive correctness Jibril lives for~ ♡recollection_tool_test.dart— 8 tests covering real updates with state echo, both no-op variants (withupdated_atstability asserted against the raw DB row — chef's kiss), same-value-omitted-from-changed-list, rename via id+title, rename collision, expiration re-arm, and the unknown-title error. Every new branch has a home. fufu~ you even assert thatSqliteExceptiondoes not leak on collision. That's the kind of defensive assertion that makes me trust a test suite~recollection_tool.dart:271-278) mirrors_create's friendlygetByTitleguard, and you correctly exempt a rename-to-own-title (it falls through to the same-value no-op path). The asymmetry I flagged last round is gone._formatRecollectionextraction stays clean —readandupdatespeaking the same language is still wonderful.💡 Little ideas (non-blocking)~
getByIdis not assistant-scoped —recollection_repository.dart:33.getByTitlefilters onassistant_id, butgetById(int id)doesWHERE id = ?with no assistant guard. Today this is harmless (the tool resolves by title or trusts the caller's id), but it's a latent cross-assistant data-access hole: if an assistant ever obtains another assistant's recollection id (leaked in a log, a guess), anupdate/read/deleteby id would touch it. Consider addingAND assistant_id = ?togetByIdand threading the id through. Pre-existing, unrelated to this PR's intent — just planting the seed since you're clearly someone who cares~ ♪Ship it~ ♡
Automated review by Jibril · 2026-07-04
CI/CD: absent for head SHA
a4f00a5· Local checks: re-read the full diff + full current file;dart analyze/dart testnot re-run in this review context (no Dart SDK in the review sandbox), relying on your reporteddart test: 8/8+dart analyze: no issues