Scope
How pair scoping prevents guardrail state and cache files from bleeding across pairs and modes.
Scope prevents “state bleed” between different pairs.
It keeps guardrail files separate across:
pair direction (one-way vs two-way)
provider combination
pair index/id
When scope matters
You’ll feel it when you:
duplicate a pair
change a pair’s mode
clear cache and expect only one pair to reset
Quick troubleshooting
If you see files named ...unscoped...:
the run didn’t set scope correctly
guardrail files may collide across pairs
Related:
Where these files live: State
The most scope-sensitive guardrails: Phantom Guard, Unresolved
This doc explains how the orchestrator scopes state/guardrail files per pair and feature, and why you sometimes see filenames that look like a password.
Implementation notes
Core code: cw_platform/orchestrator/_scope.py
Where scope is set: cw_platform/orchestrator/_pairs.py (_pair_env)
Used by: _phantoms.py, _blackbox.py, _unresolved.py, _snapshots.py (shadow), and other helpers.
Why scope exists
Without scoping, guardrail state would collide:
A phantom from
PLEX → SIMKLwatchlist would blockPLEX → TRAKTtoo.A blackbox entry from one feature could affect a different feature.
Multiple configured pairs (pair 0, pair 1…) would share the same cooldown files.
Scope makes these state files:
pair-specific
mode-specific (one-way vs two-way)
and usually feature-specific
So failures don’t leak across unrelated sync paths.
What “scope” is (in practice)
Scope is a short string derived from env vars the orchestrator sets right before running a feature.
Typical scopes look like:
one-way_plex-simkl_0two-way_plex-trakt_1
There’s also:
healthscope (temporary) during the health passa fallback “unscoped/default” when scope isn’t set (should be rare in real runs)
Where scope comes from
In _pairs.py, before each feature run, orchestrator calls _pair_env(...) which sets env vars:
CW_PAIR_SCOPE← the main scope stringCW_PAIR_KEY← the provider-pair key (providers sorted)CW_PAIR_SRC/CW_PAIR_DSTCW_PAIR_MODE←one-wayortwo-wayCW_PAIR_FEATURE←watchlist,ratings, etc.(compat)
CW_PAIR,CW_SYNC_PAIR
Scope key construction
pair key:
"-".join(sorted([src, dst]))(e.g.,PLEX-SIMKL)index/id: uses
pair.get("id")if present, else config indexifinal:
"{mode}:{pair_key}:{id_or_index}"
Then it gets sanitized to be filename-safe:
lowercased
non
[a-z0-9._-]replaced with_:becomes_capped to 96 characters
So:
two-way:PLEX-SIMKL:0→two-way_plex-simkl_0
_scope.py helpers
_scope.py helperspair_scope() -> str
pair_scope() -> strReturns os.environ["CW_PAIR_SCOPE"] if set, else "unscoped".
pair_key() -> str
pair_key() -> strReturns os.environ["CW_PAIR_KEY"] if set, else "unscoped".
scoped_file(prefix: str, ext: str) -> str
scoped_file(prefix: str, ext: str) -> strReturns a full path under .cw_state:
/config/.cw_state/{prefix}.{scope}.{ext}
Example:
scoped_file("SIMKL_watchlist", "blackbox.json")→/config/.cw_state/SIMKL_watchlist.one-way_plex-simkl_0.blackbox.json
This is used heavily by guardrail modules.
Which files are scoped vs global
Usually scoped
PhantomGuard:
watchlist.{src}-{dst}.{scope}.phantoms.jsonwatchlist.{src}-{dst}.{scope}.last_success.json
Unresolved:
{dst}_{feature}.{scope}.unresolved.pending.json(expected)
{dst}_{feature}.{scope}.unresolved.json
Blackbox flap counters:
{dst}_{feature}.{scope}.flap.json
Some blackbox entries (depending on config / pair_scoped):
either
{dst}_{feature}.{pair}.blackbox.jsonor{dst}_{feature}.{scope}.blackbox.json
ANILIST shadow:
anilist_watchlist_shadow.{scope}.json
Not scoped
/config/state.json,/config/state.manual.json,/config/last_sync.json/config/.cw_state/tombstones.json(single file, but entries are feature+pair-scoped inside its keys)
Pair key vs scope (they’re not the same)
Two different concepts:
Pair key
"PLEX-SIMKL"(providers sorted)used to share state across multiple configured “pairs” that happen to be the same provider combo
Blackbox supports pair-key scoping (optional) because:
flappers tend to be “provider combo”-specific, not “config row”-specific
Tombstones always use pair key inside the JSON key.
Scope
includes mode and pair index/id
isolates state per configured pair
PhantomGuard uses scope to avoid cross-pair contamination.
Common confusion patterns
“Why does my file name include the pair index?”
Because scope includes pair_id_or_index. If you duplicate a pair in config, you get separate scope files.
“Why do I have multiple phantom files for the same provider pair?”
Because different pair entries (or mode) generate different scopes.
“Why is there a blackbox file with PAIR but not SCOPE?”
Because _blackbox.py can be configured to store entries per pair key:
{dst}_{feature}.{PAIR}.blackbox.json
This is separate from flap counters, which are usually scoped.
Debugging scope issues
Inside the container, while a run is in progress, you can check:
If you see “unscoped” during a real feature run, something isn’t setting env correctly.
To inspect all guardrail files for a specific pair run:
To find which files belong to a pair key (regardless of scope):
Operational advice
If you want a guardrail to apply across all config rows for the same provider combo, use pair key.
If you want it isolated per configured pair, use scope.
Don’t mix the two in one mechanism unless you’re very sure; it’s how you get “why is TRAKT blocked by SIMKL’s failures” moments.
Related pages
File locations and naming patterns: State
PAIR vs SCOPE behavior: Blackbox
Scope-heavy files: Phantom Guard, Unresolved
Pair key usage for tombstones: Two-way sync
Last updated