Skip to content

Stop memory writes reporting topics as lost - #502

Merged
sysread merged 1 commit into
mainfrom
claude/recipe-editor-empty-lists-uhm8kt
Aug 17, 2026
Merged

Stop memory writes reporting topics as lost#502
sysread merged 1 commit into
mainfrom
claude/recipe-editor-empty-lists-uhm8kt

Conversation

@sysread

@sysread sysread commented Aug 17, 2026

Copy link
Copy Markdown
Owner

SYNOPSIS

Audit of all 56 tools for the bug #501 fixed in recipe_update. Two
memory tools had it.
One tool description promised an outcome its
code does not guarantee.

PURPOSE

#501 fixed a tool that reported an empty photo list and empty topic
list
on an edit that had preserved both, and the model relayed it as
data loss. That bug has a shape - a response asserting a field
instead of reading it, or echoing a column a trigger had just wiped -
and nothing guaranteed it was confined to one tool.

DESCRIPTION

Where it was. memory_update and memory_reshape selected the
written row back with topics. The label/data edit that motivated
the write also fires clear_memory_topics_on_change, which empties
that column to re-queue the row for the memory-topics curation unit, and
RETURNING reads the row after the trigger. So the field reported
an empty tag list at exactly the moment the model had edited the text.

  • reshape is the worse of the two - it requires a label or data
    change, so its topics could never have been anything but empty.
  • update is the sneakier - accurate on a confidence-only patch,
    a lie on a text patch, indistinguishable from the outside.

What changes.

  • Both tools drop topics from the select. Keeping it on update's
    confidence-only path would be worse than omitting it: a
    sometimes-true field the model can't tell apart from a re-queued one
    is harder to reason about than no field. memory_search /
    memory_get are the read-back paths.
  • record_delete's description promised {deleted: true}; the code
    deliberately answers {deleted: false} for an id matching
    nothing. Description now says so and tells the model to check.
  • tests/memory_write_shape.test.ts, 2x cases, asserting on the
    SELECT column list rather than the response - a stub returning a
    fixed row would pass even if someone re-added the column.

How that closes it. Every tool response now describes state
actually read
, and the guard is on the query, not the payload.

Audited and deliberately unchanged (flagging these so they don't
read as misses):

  • The unconditional {deleted: true} on memory_delete,
    recipe_delete, memory_unrelate, wiki_delete. Looks like the same
    bug; isn't. It exists so a caller can't probe for another
    user's rows by id
    - "doesn't exist" and "isn't yours" must look
    identical. Documented in each file.
  • memory_create and recipe_save's empty lists: an insert never
    fires the re-tag trigger and links no images, so honestly empty.
  • doc_read's empty lines: the out-of-range branch, which carries a
    note explaining it.
  • The embedding-clearing triggers - verified no tool selects an
    embedding
    , so they can't leak this way.
  • Every read tool's promised field list matches its select
    (spot-checked recipe_list, record_get, doc_get).

Notes:

  • Dropping topics from two write responses is an intentional shape
    change. Adding it back re-arms the false alarm.
  • The trigger behavior itself is correct and untouched; only the
    reporting of it changed.
  • Live data confirms the pipelines are healthy: 500/500 memories and
    40/40 recipes currently tagged. Nothing was ever lost - only
    misreported.

Generated by Claude Code

Audit of the other tools for the bug just fixed in recipe_update. Two
memory tools had it, and one tool description promised an outcome its
code does not guarantee.

memory_update and memory_reshape selected the row back with topics
included. The label/data edit that motivated the write also fires
clear_memory_topics_on_change, which empties that column to re-queue the
row for the memory-topics curation unit, and RETURNING reads the row
after the trigger - so the field reported an empty tag list at exactly
the moment the model had edited the text. reshape is the worse of the
two: it requires a label or data change, so its topics field could never
have been anything but empty. Both now leave the column out of the
select. Keeping it on memory_update's confidence-only path, where the
trigger does not fire and the tags do survive, would be worse than
omitting it - a sometimes-true field the model cannot tell apart from a
re-queued one is harder to reason about than no field.

memory_create keeps its topics: an insert never fires the trigger, so an
empty list there is the honest "no tags yet", same as recipe_save's
empty photo list.

record_delete's description promised {deleted: true}, but the code
deliberately answers {deleted: false} for an id that matches nothing.
The description now says so and tells the model to check the flag.

Also audited and deliberately left alone: the unconditional
{deleted: true} on memory_delete, recipe_delete, memory_unrelate, and
wiki_delete, which exists so a caller cannot probe for another user's
rows by id; doc_read's empty line list, which carries a note explaining
the out-of-range request; and the embedding-clearing triggers, which no
tool echoes.
@sysread
sysread merged commit 559c2bc into main Aug 17, 2026
1 check passed
@sysread
sysread deleted the claude/recipe-editor-empty-lists-uhm8kt branch August 17, 2026 22:42
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