[Bug] meta.rewardDirty is re-set on episode reopen and never cleared when the episode later reaches a terminal state
Pre-submission checklist | 提交前检查
- I have searched existing issues and this hasn't been mentioned before | 我已搜索现有问题,确认此问题尚未被提及
- I have read the project documentation and confirmed this issue doesn't already exist | 我已阅读项目文档并确认此问题尚未存在
- This issue is specific to MemOS and not a general software issue | 该问题是针对 MemOS 的,而不是一般软件问题
Bug Description | 问题描述
meta.rewardDirty is written when an episode is reopened, but if that episode subsequently reaches a terminal state through a path other than the ones that clear the marker, the marker is never removed. It is then indistinguishable from a live dirty state to both episodeRewardIsDirty() and a consistency watchdog built on json_type(meta_json,'$.rewardDirty') IS NOT NULL, so the episode is reported as dirty on every run — indefinitely — while being fully resolved.
Observed on a live installation: episode ep_ewndcv3q2ngk, 7 traces.
- scored heuristically and terminal-skipped —
meta.reward.skipped: true,traceCount: 2(1 turn, below the 2-exchange minimum) - reopened 89 s later —
meta.rewardDirty.reason: "episode_reopened",reopenedFor: "follow_up" - later recovered via
recoveryReason: "dirty_reward_rescore",closeReason: "finalized",rTask = 0.0
json_type(meta_json,'$.rewardDirty') still returns object 17 days later:
{"reason":"episode_reopened","reopenedFor":"follow_up","at":<ts>,"previousRTask":0,"previousScoredAt":<ts>}Manual json_remove(meta_json,'$.rewardDirty') clears every symptom immediately. Note that json_set(meta_json,'$.rewardDirty',NULL) does not — json_type then returns 'null', which is still IS NOT NULL, so any check written as a null-test keeps firing.
How to Reproduce | 如何重现
Not yet reproduced deterministically; the above is a live record. The ordering that appears to be required:
- Episode is scored and terminal-skipped → clears the marker (
core/reward/reward.ts:135). - Episode is reopened while
rTask != null || meta.reward→ re-sets the marker (core/session/episode-manager.ts:337). The skip-time clear has already run and will not run again. - Episode later reaches a terminal state through the recovery path, which finalizes it without touching the marker.
- Marker persists.
Expected: reaching a terminal/recovered state clears rewardDirty regardless of which path produced it.
Actual: marker persists; the episode is reported dirty forever.
Relevant code (verified against main, de806942)
Set:
core/session/episode-manager.ts:337— on reopen, gated onsnap.rTask != null || snap.meta.reward
Clear:
core/pipeline/memory-core.ts:1720— clears only inside the prior-failed-attempts rescan loop, and only for episodes that pass through it this passcore/pipeline/memory-core.ts:1757— clearsskipped && hasRewardDirtyMarkerrows, but over a bounded pagecore/reward/reward.ts:135and:276— clear at the moment a skip/terminal reward is written
The last three all clear at a specific moment or over a bounded page. Step 2 above re-dirties the episode after the moment-clear has passed, and the remaining two are the only later opportunities — so an episode that misses both keeps the marker permanently. Whichever of those two should have caught this row did not, which is the part worth confirming on the maintainer side; the durable fix is probably to drop the marker wherever the episode reaches a terminal reward state, rather than at each site that can write one.
Environment | 环境信息
- memos-local-plugin 2.0.16
algorithm.lightweightMemory.enabled: false- Linux (Debian-based), Node 22
- re-verified against upstream
main@de806942
Additional Context | 其他信息
Impact: an operator-facing consistency check reports the row on every run forever. That trains the reader to ignore the check, which is the same class of observability decay that made a separate monitoring review misdiagnose frozen metrics.
Relevant prior issues reviewed: #1847, #1784, #2120, #1966 — all different failure modes (dirty-scan/skip-path fixes, abandoned-episode skip, dirty-closed pagination, ghost-trace over-scoring). I could not find an existing report of the marker surviving on a resolved episode.
Source: MemTensor/MemOS