Issue Description
In Stalwart 0.16.22, spam_train(false) loads the persisted trainer and selects samples starting at trainer.last_id + 1. However, last_id is updated only when it equals zero, so it stops advancing after the first successful training run.
Retained samples added afterward are consequently processed again in later runs. Deduplication is local to each run and does not prevent this.
This affects ordinary user feedback: add_account_spam_sample(), used by IMAP spam/ham corrections, passes hold_sample=true, producing delete_after_use=false.
Verified through source inspection and an isolated reproduction of the selection/counter logic. The same code is present on main.
Expected Behavior
After successful incremental training, the persisted cursor should advance past the newly processed samples.
A subsequent run with no new samples should perform no training and leave the ham/spam total_seen counters unchanged.
Actual Behavior
The cursor remains at the highest sample ID from the first successful training run. Later retained samples are repeatedly treated as new, incrementing counters and being submitted to training again.
An isolated control-flow reproduction produced:
Run 1: 100 ham + 100 spam added
Processed: 200; last_id: 200; ham: 100; spam: 100
Run 2: one new ham + one new spam added
Processed: 2; last_id: 200; ham: 101; spam: 101
Run 3: nothing added
Processed: 2; last_id: 200; ham: 102; spam: 102
IDs are simplified. The balanced classes exclude intentional reservoir replay for class balancing as the explanation.
At the default 12-hour interval, a retained sample could be processed approximately 360 times over 180 days if runs succeed and nothing removes the sample or resets/rebuilds the trainer. This is a scheduling-based estimate, not a production measurement. Classification-accuracy impact has not been measured.
Reproduction Steps
Suggested integration reproduction for an isolated instance; not executed here:
-
Start with a fresh classifier and 100 distinct valid ham samples plus 100 distinct valid spam samples. Set delete_after_use=false and keep their blobs available.
-
Run SpamFilterMaintenance with maintenanceType=“train” and wait for successful completion.
-
Add two new distinct retained samples: one ham and one spam.
-
Run maintenanceType=“train” again and wait for completion.
-
Without adding or changing samples, run maintenanceType=“train” a third time.
-
Inspect SpamTrainer.last_id, the reservoir ham/spam total_seen counters, and the training-completed event.
The code path predicts that the third run reprocesses the two samples from step 3 instead of reporting zero work. Use “train”, not “retrain”.
Relevant Log Output
No live-server training logs are attached. The results above come from an isolated reproduction of the source control flow, not from running the Stalwart binary.
Stalwart Version
v0.16.x
Installation Method
Binary (Linux)
Database Backend
RocksDB
Blob Storage
RocksDB
Search Engine
Internal
Directory Backend
Internal
Additional Context
Selection, deduplication and cursor assignment:
Trainer persistence:
Counter increment:
User-generated samples are retained:
I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on