Phantom Guard
Watchlist anti-flap guard that blocks “ghost adds” that don’t stick on the destination.
Phantom Guard blocks “ghost adds” on watchlists.
These are adds that look successful, but never appear later.
What you’ll see
The same watchlist item keeps planning “add”.
Then it becomes blocked for a cooldown window.
What to do
If you actually want the item, remove it from the phantoms file.
If this happens often, suspect mismatched IDs or stale provider snapshots.
Related:
Staleness troubleshooting: Caching layers
PhantomGuard is the watchlist-specific anti-flap guard: it blocks “ghost adds” that appear to succeed but never actually stick on the destination.
It exists because watchlist APIs often:
accept an add,
return 200 OK,
but the item doesn’t show up later (bad IDs, region, media type mismatch, internal queueing, etc.).
Code: cw_platform/orchestrator/_phantoms.py
Used by: one-way and two-way pipelines for watchlist (and partially ratings).
What it blocks
PhantomGuard blocks planned ADDs (never removes).
Typical usage in pipelines:
For
watchlist: filter all adds through PhantomGuard.For
ratings: only filter “fresh adds”; don’t filter rating updates.
So PhantomGuard is not a general-purpose “cooldown”; it’s “this add didn’t stick before”.
Data tracked
PhantomGuard tracks two sets per feature + src + dst + scope:
Phantoms: keys that were attempted as adds but did not later appear in destination inventory.
Last success: keys that were last observed as successful (used for decay heuristics).
Files (scoped) live under /config/.cw_state/:
{feature}.{src}-{dst}.{SCOPE}.phantoms.json{feature}.{src}-{dst}.{SCOPE}.last_success.json
Example:
watchlist.PLEX-SIMKL.one-way_plex-simkl_0.phantoms.json
File formats
Phantoms file
Last success file
Keys are canonical keys.
TTL / cooldown logic
PhantomGuard uses a TTL in days:
ttl_dayspassed in by pipeline
In current wiring, TTL comes from root config:
cfg["blackbox"]["cooldown_days"]
Yes, naming is confusing. PhantomGuard is not _blackbox.py.
Expiry rule (conceptual):
if
now - last_seen > ttl_days * 86400→ phantom entry is removed or ignored
So “phantom” is a temporary state, not permanent.
How it decides something is a phantom
PhantomGuard relies on the pipeline to call it at the right times:
1) Before applying adds
guard.filter_adds(items)removes any item whose key is in the active phantom set.
2) When an add is attempted
guard.record_attempt(keys)is called for the keys that were tried.
This increments attempts and updates timestamps.
3) When an add is confirmed as successful
guard.record_success(keys)updateslast_successtimestamps and can clear phantom entries.
This is called when:
provider confirmed keys are determined (either from confirmed_keys or approximated)
4) When a later snapshot shows presence (implicit success)
Some flows treat “item is now present in dst snapshot” as success and clear phantom status.
Whether that is used depends on how pipelines refresh/bust snapshots and how providers build indices.
Net effect:
repeated attempts without subsequent presence → phantom → blocked for TTL.
Why it’s watchlist-focused
Watchlist adds are the most likely to “look successful” but not stick because:
providers silently reject “wrong kind” of item (movie vs show mismatch)
list is region/account-specific
item lacks required metadata/ID mapping
provider queues updates (eventual consistency)
For history/ratings/removes, failures are usually explicit (API returns an error), so blackbox/unresolved are better fits.
Relationship to Blackbox / Unresolved / Tombstones
PhantomGuard: “add did not stick; block future adds temporarily” (watchlist-centric)
Blackbox: “repeated failures; stop retrying for cooldown” (non-watchlist adds in current wiring)
Unresolved: “this key failed to apply; don’t try next run” (but needs pending→active wiring)
Tombstones: “this token was deleted; don’t re-add (and maybe propagate delete)” (two-way safety)
They overlap in spirit but solve different failure patterns.
Manual operations
Inspect phantom files
Inside container:
ls /config/.cw_state/watchlist.*.phantoms.jsonopen the relevant file for your pair scope.
Unblock one item
Remove the key from the .phantoms.json file.
Reset PhantomGuard for a pair
Delete both:
*.phantoms.json*.last_success.json
Common symptom patterns
“Watchlist keeps planning the same adds forever”
PhantomGuard should eventually block those adds once attempts accumulate.
If it doesn’t, the provider is probably reporting confirmed keys too optimistically or snapshots are stale.
“Item is blocked but I actually want it”
Remove it from the phantoms file (or lower cooldown).
“Everything is blocked suddenly”
Wrong scope file (pair env) can cause cross-contamination if scope key is too generic.
Verify the filename scope matches your pair/mode.
Related pages
Different system for non-watchlist adds: Blackbox
Where PhantomGuard is applied: One-way sync, Two-way sync
Snapshot caching and “stickiness”: Snapshots, Caching layers
Last updated