Repository navigation
Description
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
Bug Description | 问题描述
This is a bug in the MemOS local plugin (apps/memos-local-plugin, @memtensor/memos-local-plugin), not in the Python memoryos package.
When a user edits an earlier query and resends it, the host forks the conversation and the previous assistant answer is no longer part of the current branch. If that answer was already stored by turn.end, the local plugin still treats it as something that happened.
turn.start does not know which branch is current. It only hides traces that are still inside the model-visible window of the same session (visibleContextStartTs in core/retrieval/retrieve.ts, applied by core/retrieval/tier2-trace.ts as session_id != current OR ts < window start). Traces older than that window stay eligible. An abandoned branch that has fallen out of the window is therefore easier to recall as historical evidence than the recent turns the model can still see. There is no fixed "last 8 turns" constant. A window of eight is simply one window length; a shorter window recalls still more of the older same-session traces. When the host cannot report a window, the whole session is excluded instead.
The same stored chain shares one terminal task reward. core/reward/backprop.ts computes V_T = R and V_t = alpha * R + (1 - alpha) * gamma * V_next, with alpha clamped to [0, 1] and gamma defaulting to 0.9. A later negative sentence lowers every earlier step whose alpha is not zero, including steps that were already correct. Setting the abandoned step's alpha to 0 or to a negative value does not keep the correct step out of that negative reward.
An L3 world-model entry that already cites the abandoned trace keeps that citation. combineDraftEntries only unions evidence ids, and mergeForUpdate keeps an entry whose label the next draft does not rewrite. Nothing revokes an evidence id because its source turn left the current branch.
The word "abandoned" here is the discarded answer. It is not an episode closed by timeout (closeReason = abandoned) and it is not rewardDirty, which only marks a failed scoring attempt for retry.
Expected: retrieval, task credit, and L3 evidence follow the branch the user kept. The discarded answer stays stored, but it is not recalled as something that happened, a later negative reward does not lower earlier correct steps outside that feedback, and an L3 citation of the discarded trace is revoked.
Memmy's local L1/L2/L3 memory uses the same kind of chain: one chronological trace log, recall that hides only the visible window, one terminal reward shared by alpha, and L3 evidence ids that accumulate. This report did not run Memmy. If Memmy still has no current-branch head for an edited query, it can show the same three failures.
中文补充: "The abandoned answer is the assistant output the user discarded by editing an earlier query and resending it." 这句里的 abandoned answer 指用户改掉历史 query 并重发后丢掉的那次回答,不是 episode 超时关单,也不是 rewardDirty。
How to Reproduce | 如何重现
- In one host session (OpenClaw, Hermes, or DeepSeek Harness) with the local plugin, complete a turn that contains a correct step and an assistant answer.
turn.endstores those traces. - Edit an earlier query and resend it, so the host continues from the new branch and the previous answer is no longer visible.
- Continue on that branch until the model-visible window contains only the recent completed turns of the new branch. The window observed in practice was the last 8 completed turns; the plugin does not hardcode 8.
- Send the next query.
turn.startrecalls the discarded answer from outside the window as historical evidence. - Later, give one negative sentence about the current outcome. The plugin scores one task reward and backpropagates it through the stored steps. The earlier correct step is lowered with the discarded step.
- If an L3 entry was written with the discarded trace in
evidenceIds, a later merge that does not rewrite that entry leaves the citation in place.
The measurements below are not a live host transcript. They call the production functions on rows with the same shape those functions read after both the discarded answer and the new branch have been stored: one session, one episode, traces in time order correct-step, orphan-answer, then window-0 through window-7. The eight-turn window starts at window-0. Alphas are 0.8, 0.6, and 0.9 on correct-step, orphan-answer, and window-7. R = -0.8, gamma = 0.9. The L3 entry already cites orphan-answer; the next draft adds a different label and does not mention it.
runTier2 with that eight-turn window returned 2 traces: correct-step and orphan-answer. window-0 through window-7 were excluded. Moving the window start to window-5 (the last 3 turns) returned 7 traces: the same two, plus window-0 through window-4.
backprop on correct-step, orphan-answer, window-7 in that order, without the middle window turns, produced V = -0.8, -0.768, and -0.77824. With the orphan alpha set to 0, the correct step was still -0.7696. With the orphan alpha left unclamped at -1, the correct step was still -0.7552.
mergeForUpdate kept both environment entries, and orphan-answer was still in the evidence ids.
What this input shows is the filter and the formula after the rows exist. It does not show the host message that creates the fork, and it does not show that every host uses a window of 8.
Environment | 环境信息
- Python version: 3.9.6.
pip show memoryosfinds no installed package. The failure is in the TypeScript local plugin, not in that package. - Operating System: macOS 27.0.1 (darwin 27)
- MemOS version: not installed via pip. Local plugin
@memtensor/memos-local-plugin2.0.16-beta.1, Node.js v26.11.0, vitest 2.1.9.
Additional Context | 其他信息
A negative alpha is the wrong repair. Alpha is the share of the terminal reward, and (1 - alpha) becomes larger than 1 when alpha is negative, so the correct step still receives the negative reward. rewardDirty is also the wrong signal: it is cleared after a scoring retry and does not record which branch was kept.
The missing record is a parent turn and, when the user edits a query, the turn it replaces. Old turns stay stored. Retrieval, reward, and L3 evidence follow only the current head. The negative reward applies to the turn it refers to and to turns that descend from it. An evidence id whose source turn is off that head is marked revoked. Alpha stays in [0, 1].
Willingness to Implement | 实现意愿
- I'm willing to implement this myself
Activity