You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A ghost trace ID () was added to an episode's but never existed in the table. This caused to return indefinitely, triggering 590 reward rescore calls over 5 days and wasting ~14.5 RMB on LLM calls.
Root Cause
1. Ghost trace creation
The episode was reopened with . During the follow-up merge, was called which directly overwrites without validating that trace IDs actually exist in the table.
// episodes.js line 80-82appendTrace(id,traceIds){appendTrace.run({ id,trace_ids_json: toJsonText(traceIds)});// No validation that traceIds exist in traces table!}
2. Reward traceCount mismatch
loads traces via which only returns existing traces:
// reward.js line 48-52consttraces=traceIds.length>0
? deps.tracesRepo.getManyByIds(traceIds).sort(...)
: [];
This returns 11 traces (ghost excluded). After scoring: reward.traceCount = 11.
3. Dirty check compares against episode.traceIds.length
Summary: Fix #1966 — the reward dirty-check infinite loop triggered by ghost trace IDs lingering in episodes.trace_ids_json (the reporter wasted 590 LLM calls / ~14.5 RMB over 5 days). The bug lives in apps/memos-local-plugin: episodeRewardIsDirty compared reward.traceCount against the raw ep.traceIds.length, so a single orphan ID kept the episode dirty forever and runDirtyClosedRewardScan re-scored it every 10 minutes.
Three-layer fix (defence in depth): (A) episodeRewardIsDirty now compares reward.traceCount against the count of trace IDs that actually exist in the traces table, via a new traces.countExisting helper (single SELECT COUNT(*) per 900-id chunk, no BLOB hydration — same cost profile as the hasAnyNewerThan fix added for #1787). This deterministically breaks the loop. (B) repos.episodes.appendTrace now strips ghost IDs before writing trace_ids_json, so new ghosts cannot accumulate. (C) Two new traces-repo helpers — countExisting(ids) and filterExistingIds(ids) — back both fixes with the same chunked-SELECT pattern. The issue's other two suggested fixes (retry counter, startup ghost-scan) were intentionally skipped: Fix A breaks the loop without them, and adding either would slow init or introduce extra state to reason about.
Tests: tests/unit/storage/repos.test.ts updated to match the new appendTrace contract (insert real traces first, then assert ghost-strip on mixed / all-ghost / re-append inputs) and gains coverage for both new helpers; tests/unit/pipeline/memory-core.test.ts adds an integration test "does not rescore a closed episode whose only mismatch is a ghost trace ID (#1966)" that seeds the exact reporter scenario and asserts r_task and meta.reward.rHuman stay untouched after init(). npm run lint passes clean; npx vitest run tests/unit → 1041 passed, only 2 pre-existing failures remain (migrator schema-column drift and a traces-count pagination cap, both confirmed to fail on dev-20260624-v2.0.22 via git stash and both unrelated to ghost-trace handling). No DB migrations, no schema changes, no API surface changes; all 4 existing appendTrace callers insert trace rows before referencing them, so the new validation is a no-op for valid inputs.
Activity
Memtensor-AI commentedon Jun 30, 2026
🤖 AutoDev has picked up this issue and started working on it.
Task ID:
35fc16a4a540b16aWorking branch:
feature/autodev-1966Target branch:
latest dev* branchWorkflow: opsp (analysis → coding → testing → PR)
I will post the PR link here once done. If I need more information, I will ask in the comments.
fix(memos-local-plugin): tolerate ghost trace IDs in reward dirty che…
Memtensor-AI commentedon Jun 30, 2026
✅ AutoDev task
35fc16a4a540b16acompleted.Summary: Fix #1966 — the reward dirty-check infinite loop triggered by ghost trace IDs lingering in
episodes.trace_ids_json(the reporter wasted 590 LLM calls / ~14.5 RMB over 5 days). The bug lives inapps/memos-local-plugin:episodeRewardIsDirtycomparedreward.traceCountagainst the rawep.traceIds.length, so a single orphan ID kept the episode dirty forever andrunDirtyClosedRewardScanre-scored it every 10 minutes.Three-layer fix (defence in depth): (A)
episodeRewardIsDirtynow comparesreward.traceCountagainst the count of trace IDs that actually exist in thetracestable, via a newtraces.countExistinghelper (singleSELECT COUNT(*)per 900-id chunk, no BLOB hydration — same cost profile as thehasAnyNewerThanfix added for #1787). This deterministically breaks the loop. (B)repos.episodes.appendTracenow strips ghost IDs before writingtrace_ids_json, so new ghosts cannot accumulate. (C) Two new traces-repo helpers —countExisting(ids)andfilterExistingIds(ids)— back both fixes with the same chunked-SELECT pattern. The issue's other two suggested fixes (retry counter, startup ghost-scan) were intentionally skipped: Fix A breaks the loop without them, and adding either would slow init or introduce extra state to reason about.Tests:
tests/unit/storage/repos.test.tsupdated to match the newappendTracecontract (insert real traces first, then assert ghost-strip on mixed / all-ghost / re-append inputs) and gains coverage for both new helpers;tests/unit/pipeline/memory-core.test.tsadds an integration test "does not rescore a closed episode whose only mismatch is a ghost trace ID (#1966)" that seeds the exact reporter scenario and assertsr_taskandmeta.reward.rHumanstay untouched afterinit().npm run lintpasses clean;npx vitest run tests/unit→ 1041 passed, only 2 pre-existing failures remain (migrator schema-column drift and a traces-count pagination cap, both confirmed to fail ondev-20260624-v2.0.22viagit stashand both unrelated to ghost-trace handling). No DB migrations, no schema changes, no API surface changes; all 4 existingappendTracecallers insert trace rows before referencing them, so the new validation is a no-op for valid inputs.Base branch:
mainBranch:
feature/autodev-1966Commit:
ed6d25fPR: #2004
Assigned to: @MatthewZhuang, @CarltonXiang, @syzsunshine219, @World-controller
Reviewers: @MatthewZhuang, @CarltonXiang, @syzsunshine219, @World-controller
Fix #1966: bug: ghost trace in episode.trace_ids_json causes infinite…
Fix MemTensor#1966: bug: ghost trace in episode.trace_ids_json causes…