# Phantom Guard

{% tabs %}
{% tab title="End users" %}
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](/blueprint-architecture/orchestrator/caching-layers.md)
  {% endtab %}

{% tab title="Power users" %}
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`:

1. **Phantoms**: keys that were attempted as adds but did not later appear in destination inventory.
2. **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

```json
{
  "tmdb:278": { "first_seen": 1738090000, "last_seen": 1738100500, "attempts": 1 },
  "imdb:tt1234567": { "first_seen": 1738100000, "last_seen": 1738101000, "attempts": 3 }
}
```

#### Last success file

```json
{
  "tmdb:550": 1738091111,
  "imdb:tt0111161": 1738099999
}
```

Keys are canonical keys.

***

### TTL / cooldown logic

PhantomGuard uses a TTL in days:

* `ttl_days` passed 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)` updates `last_success` timestamps 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.json`
* open 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

1. “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.

2. “Item is blocked but I actually want it”

* Remove it from the phantoms file (or lower cooldown).

3. “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](/blueprint-architecture/orchestrator/blackbox.md)
* Where PhantomGuard is applied: [One-way sync](/blueprint-architecture/orchestrator/one-way-sync.md), [Two-way sync](/blueprint-architecture/orchestrator/two-way-sync.md)
* Snapshot caching and “stickiness”: [Snapshots](/blueprint-architecture/orchestrator/snapshots.md), [Caching layers](/blueprint-architecture/orchestrator/caching-layers.md)
* File locations: [State](/blueprint-architecture/orchestrator/state.md), [Scope](/blueprint-architecture/orchestrator/scope.md)
  {% endtab %}
  {% endtabs %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wiki.crosswatch.app/blueprint-architecture/orchestrator/phantom-guard.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
