Skip to content

fix(storage): stop swallowing the neuron_state write, and reclaim orphans explicitly - #186

Merged
acidkill merged 2 commits into
mainfrom
fix/neuron-state-silent-failure
Aug 27, 2026
Merged

fix(storage): stop swallowing the neuron_state write, and reclaim orphans explicitly#186
acidkill merged 2 commits into
mainfrom
fix/neuron-state-silent-failure

Conversation

@acidkill

Copy link
Copy Markdown
Owner

Closes #174.

What

Two defects in opposite directions, both silent.

1. A swallowed write reported success (#174)

add_neuron wrote the neuron, then its neuron_state row inside a bare
except: pass. A failed state write still returned the neuron id, so afterwards
"the state was written" and "the state write failed" were indistinguishable.

This matters more than a missing row. neuron_state holds access_frequency and
activation_level, so a neuron without one looks permanently un-accessed: it never
receives the activation boost, and it is a standing candidate for dead-neuron pruning —
for a reason nothing logged.

Both paths now log and re-raise. The single path additionally rolls the neuron back, so a
caller's retry does not collide with a half-written pair. The batch path does not unwind —
its neurons are already written in bulk, and a partial unwind is a worse outcome than a
loud failure the caller can retry.

2. Nothing reclaimed orphaned states

Both delete paths clean up their own state today, so surviving orphans are historical. They
are not inert: apply_decay iterates get_all_neuron_states(), so each orphan is decay
computed for a neuron that no longer exists, and it inflates any per-pass count taken from
that loop — which would directly corrupt the decay telemetry proposed in #183.

Reclaiming is explicit, not automatic

smem prune-orphan-states            # report only (default)
smem prune-orphan-states --apply    # delete

Deliberately not a consolidation stage. The detection compares neuron ids against
neuron_state.neuron_id, and the two sides spell the same neuron differently:

neuron.id              →  neuron:0001182a_b6f6_...   (_to_surreal_id folds - to _)
neuron_state.neuron_id →  0001182a-b6f6-...          (original dashed uuid)

Comparing them unnormalised marks every live state as an orphan and empties the table.
Both sides now go through the same fold, and a regression test asserts it. Given that
failure mode, the operation defaults to reporting and runs only when someone asks for it —
it is not something that should execute unattended on a schedule.

count_orphaned_neuron_states exists so the destructive path can be inspected before it is
taken; both it and the delete share one detection function, so they cannot drift apart.

Verification

  • full suite: 7260 passed, 0 failed (an earlier run showed one unrelated failure in
    test_unified_config; it passes in isolation on this branch and on main, and did not
    reproduce on re-run — cross-test state, not a regression here)
  • lint / format clean
  • dry-run exercised against a live database: reported orphans, deleted nothing, and neuron
    state coverage was verified unchanged afterwards

…hans explicitly

Two defects in opposite directions, both silent.

add_neuron wrote the neuron and then its neuron_state row inside a bare
`except: pass`. A failed state write still returned success, so "the state
was written" and "the state write failed" were indistinguishable afterwards.
That matters more than a missing row: neuron_state holds access_frequency and
activation_level, so a neuron without one looks permanently un-accessed — it
never receives the activation boost and is a standing candidate for
dead-neuron pruning, for a reason no log records (#174). Both the single and
the batch path now log and re-raise; the single path also rolls the neuron
back so a retry does not collide with a half-written pair.

Nothing ever removed states whose neuron was already gone. Both delete paths
clean up correctly today, so survivors are historical — but they are not
inert: apply_decay iterates get_all_neuron_states(), so each orphan is decay
computed for a neuron that no longer exists, and it inflates any per-pass
count taken from that loop.

Reclaiming them is an explicitly invoked command, NOT a consolidation stage:

    smem prune-orphan-states            # report only (default)
    smem prune-orphan-states --apply    # delete

The detection compares neuron ids against neuron_state.neuron_id, and the two
sides spell the same neuron differently: a record id is `neuron:<uuid>` with
`-` folded to `_` by _to_surreal_id, while the state keeps the dashed uuid.
Comparing them unnormalised marks EVERY live state as an orphan and empties
the table. Both sides now go through the same fold, a regression test asserts
it, and the operation defaults to reporting rather than deleting — the
failure mode is severe enough that it should not run unattended on a
schedule.
The reference is generated from the command tree, so adding a command
leaves it stale. Caught by the Docs Freshness gate, which derives the
truth from code rather than trusting the checked-in file.
@acidkill
acidkill merged commit 434f5f6 into main Aug 27, 2026
9 checks passed
@acidkill
acidkill deleted the fix/neuron-state-silent-failure branch August 27, 2026 16:43
@acidkill acidkill mentioned this pull request Aug 27, 2026
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.

[BUG] add_neuron swallows the neuron_state insert, so a neuron can exist with no state row

1 participant