Skip to content

fix: forward offline update threshold under the callee keyword - #83

Closed
breken-ai wants to merge 1 commit into
zjunlp:mainfrom
breken-ai:fix/offline-update-score-threshold
Closed

breken-ai wants to merge 1 commit into
zjunlp:mainfrom
breken-ai:fix/offline-update-score-threshold

Conversation

@breken-ai

Copy link
Copy Markdown

Symptom

LightMemory.offline_update(..., offline_update_trigger=True) never updates anything. It raises
before the first entry is touched:

Traceback (most recent call last):
  File "<stdin>", line 8, in <module>
  File ".../src/lightmem/memory/lightmem.py", line 455, in offline_update
    self.offline_update_all_entries(
TypeError: LightMemory.offline_update_all_entries() got an unexpected keyword argument 'update_sim_threshold'

So the scheduled/triggered offline-update path documented on the public offline_update signature
(src/lightmem/memory/lightmem.py:399) is unreachable — enabling the flag is a guaranteed crash, not a
slow or partial update.

Root cause

The two halves of one call disagree on the keyword name. Line numbers are on the base commit
8449d57:

  • src/lightmem/memory/lightmem.py:455-457 — the trigger branch calls
    self.offline_update_all_entries(update_sim_threshold = 0.8).
  • src/lightmem/memory/lightmem.py:541 — the callee declares
    def offline_update_all_entries(self, score_threshold: float = 0.9, max_workers: int = 5):.

score_threshold is the settled name, not update_sim_threshold: every other call site in the
repository already uses it — README.md:327, experiments/longmemeval/offline_update.py:41,
experiments/locomo/add_locomo.py:447, mcp/server.py:199-201, and
web/backend/app/instance.py:259. update_sim_threshold appears exactly once in the tree, at the
broken call site (git grep -c update_sim_threshold reports a single line in
src/lightmem/memory/lightmem.py on the base commit).

Nothing in the repository passes offline_update_trigger=True (git grep -n 'offline_update_trigger\s*=\s*True' returns nothing at the base commit), and the web backend reaches
the batch update through offline_update_all_entries directly rather than through
LightMemory.offline_update. That is why the mismatch survived: no in-tree caller and no test walked
the branch.

What this change does

Two files, 2 files changed, 23 insertions(+), 1 deletion(-):

  • src/lightmem/memory/lightmem.py:455-457 — forward the same value under the callee's real keyword:
    score_threshold=0.8.
  • tests/test_lightmem_offline_update.py (new, 22 lines) — one regression test that drives the real
    LightMemory.offline_update and asserts the keyword that reaches the callee.

Trade-off a reviewer should weigh

I kept the explicit 0.8. The alternative is to drop the argument entirely and let the callee default
of 0.9 apply. I chose 0.8 because it is the value the call site has always tried to pass, and it
matches most of what the other paths pass (README.md:327 and
experiments/longmemeval/offline_update.py:41 both use 0.8, and web/backend/app/api.py:185
defaults its score_threshold request field to 0.8; the one in-tree exception is
experiments/locomo/add_locomo.py:447, which passes 0.9). Since this branch was never executable,
either choice is technically a free pick — if you would rather the trigger inherit the callee default, that is a
one-line change and the test's expected value moves with it.

The test uses unittest.mock.patch.object(LightMemory, "offline_update_all_entries", autospec=True)
rather than a bare Mock(). autospec binds the recorded call against the callee's real
signature, so a rename on either side of the pair fails the test. A bare Mock() would accept any
keyword and would only pin what the caller passes — both directions are demonstrated in the revert
proof below.

Testing

Environment: macOS (arm64), CPython 3.11.15 (the repo requires >=3.10,<3.12, pyproject.toml:10),
pytest 9.1.1, black 26.5.1, isort 9.0.1, flake8 7.3.0 — the four tools the dev extra declares
(pyproject.toml). Everything below was run in a clean git worktree checkout of this branch.

The repository has no conftest.py and no pytest configuration, and lightmem is not installed in
the environment, so PYTHONPATH=src is required for collection to succeed at all. That is a
pre-existing condition of the checkout, not something this branch introduces.

$ PYTHONPATH=src python -m pytest tests/test_lightmem_offline_update.py -q
1 passed, 1 warning

$ PYTHONPATH=src python -m pytest tests -q
3 passed, 1 warning

The base commit's tests/ contains one file and runs 2 passed; the delta is exactly the one
new test. The single warning is a pre-existing PydanticDeprecatedSince20 raised from
src/lightmem/configs/logging/base.py:7; it does not appear on base only because base's two tests
never import that module — importing lightmem.configs.base on the unmodified base commit emits the
identical warning.

$ black --check tests/test_lightmem_offline_update.py
All done! ✨ 🍰 ✨
1 file would be left unchanged.

$ isort --profile black --check-only tests/test_lightmem_offline_update.py
(no output, exit 0)

$ flake8 --max-line-length=88 tests/test_lightmem_offline_update.py
(no output, exit 0)

$ git diff --check 8449d57..HEAD
(no output)

The repository ships no [tool.black], [tool.isort], setup.cfg, tox.ini or .flake8, so those
are black's own default line length and the black-compatible isort profile; --max-line-length=88 is
passed to flake8 so it agrees with black rather than with flake8's default of 79.

Flake8 on the modified module, base commit versus this branch:

$ flake8 --max-line-length=88 src/lightmem/memory/lightmem.py | wc -l
     183     # base commit 8449d57
     181     # this branch

Diffing the two sorted finding lists shows two deletions and nothing added:

14,15d13
< E251 unexpected spaces around keyword / parameter equals
< E251 unexpected spaces around keyword / parameter equals

That is the update_sim_threshold = 0.8 spacing disappearing with the rewritten call. The file is not
otherwise reformatted, and this branch does not make it flake8-clean — 181 pre-existing findings
remain and are deliberately left alone.

Revert proof

Both mutations were applied in the same worktree with the new test file left in place, and reverted
afterwards.

1. Revert the one-line fix (git checkout 8449d57 -- src/lightmem/memory/lightmem.py):

>               raise TypeError(
E               TypeError: got an unexpected keyword argument 'update_sim_threshold'
.../inspect.py:3184: TypeError
FAILED tests/test_lightmem_offline_update.py::test_offline_update_trigger_forwards_score_threshold
1 failed, 1 warning

Restoring the fix: 1 passed, 1 warning.

2. Rename the callee parameter instead, keeping the fix — this is the direction a bare Mock()
would not catch:

$ sed -i '' 's/def offline_update_all_entries(self, score_threshold: float = 0.9/def offline_update_all_entries(self, sim_threshold: float = 0.9/' src/lightmem/memory/lightmem.py
$ PYTHONPATH=src python -m pytest tests/test_lightmem_offline_update.py -q
E               TypeError: got an unexpected keyword argument 'score_threshold'
FAILED tests/test_lightmem_offline_update.py::test_offline_update_trigger_forwards_score_threshold
1 failed, 1 warning

Restored afterwards; git status --short clean and PYTHONPATH=src python -m pytest tests -q back to
3 passed, 1 warning.

What was NOT verified

  • The body of offline_update_all_entries never runs. The test asserts the keyword that crosses
    the call boundary and stops there. Actually consolidating entries needs a populated vector store,
    which I do not have. This PR proves the branch is reachable, not that the update it performs is
    correct.
  • No live LLM provider, embedder, Qdrant instance, FastAPI server, or GPU. The test constructs a
    LightMemory via __new__ with a stub config and a mock logger; no factory and no network call is
    exercised. The openai extra is not installed in my environment, so any path that builds a real
    memory manager fails on import before reaching this code.
  • No end-to-end run of the MCP server or the web console, both of which call
    offline_update_all_entries directly and were already passing score_threshold; they are cited as
    evidence of the intended name, not re-tested.
  • One platform, one interpreter. macOS arm64, CPython 3.11.15 only. The repo supports 3.10 as well
    and I did not run it; nothing here is version-sensitive as far as I can tell, but I did not check.
  • No performance measurement — the change forwards one float and cannot affect throughput, but I
    did not time anything.

Checklist

This repository ships no pull request template and no CONTRIBUTING.md (there is no .github/
directory at all), so the items below come from the one contribution rule stated in the README:
"We welcome contributions from the community! If you'd like to contribute, please fork the repository
and submit a pull request. For major changes, please open an issue first to discuss what you would
like to change."
(README.md:614).

  • Regression test added and shown to fail without the fix —
    tests/test_lightmem_offline_update.py, failing in both mutation directions above.
  • black / isort --profile black / flake8 --max-line-length=88 clean on the new test file.
  • PYTHONPATH=src pytest tests green (3 passed), with no pre-existing test disturbed.
  • Scope held to the defect: one line of source, one new test file, no unrelated reformatting.
  • Issue opened first — not done. This is a one-line keyword fix with no API or default change, so
    I did not read it as a "major change" under the README rule. Happy to open one if you would
    prefer every change tracked by an issue.
  • Whole-repository lint left clean — not attempted.
    src/lightmem/memory/lightmem.py still has 181 pre-existing flake8 findings; fixing them here
    would bury a one-line change in noise.

Problem
-------
`LightMemory.offline_update(..., offline_update_trigger=True)` raises
`TypeError: LightMemory.offline_update_all_entries() got an unexpected
keyword argument 'update_sim_threshold'`. The scheduled offline-update
path is therefore unreachable: any caller that enables the trigger
crashes before a single entry is updated.

Root cause
----------
`offline_update` (src/lightmem/memory/lightmem.py:455) passed
`update_sim_threshold=0.8`, but `offline_update_all_entries`
(src/lightmem/memory/lightmem.py:541) declares its parameter as
`score_threshold`. The two names were never reconciled and no test
exercised the trigger branch, so the mismatch went unnoticed.

Approach
--------
Forward the same 0.8 value under the callee's real keyword,
`score_threshold`. This preserves the behaviour the call site always
intended (an explicit threshold of 0.8 rather than the callee default
of 0.9) and is the smallest change that makes the branch executable.

Adds `tests/test_lightmem_offline_update.py`, which drives the real
`LightMemory.offline_update` method on a stubbed instance and patches
`offline_update_all_entries` with `autospec=True`, so the recorded call
is bound against the callee's real signature. A rename on either side
of the pair therefore fails the test rather than passing silently.

Verification
------------
`PYTHONPATH=src python -m pytest tests/test_lightmem_offline_update.py -q`
-> 1 passed, 1 warning. `PYTHONPATH=src python -m pytest tests -q`
-> 3 passed, 1 warning. The warning is a pre-existing Pydantic V2
deprecation raised by `src/lightmem/configs/logging/base.py`.
Reverting the one-line source change with the test in place fails it
with `TypeError: got an unexpected keyword argument
'update_sim_threshold'`; renaming the callee parameter instead fails it
with the mirror-image `'score_threshold'` error.
black, isort (black profile) and flake8 (88 columns) are clean on the
new test; the modified module compiles and its flake8 findings are a
strict subset of the same file before this change; `git diff --check`
is clean.

Impact
------
Offline batch updates can now run. No public API or default value
changes; the threshold actually applied is unchanged from what the call
site always intended.
@breken-ai

Copy link
Copy Markdown
Author

Closing as a duplicate of #80, which makes the same fix and was opened first.

@breken-ai breken-ai closed this Sep 11, 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.

1 participant