State
Where CrossWatch stores orchestrator baselines, checkpoints, and guardrail internals on disk.
State files are CrossWatch’s on-disk memory.
They let CrossWatch plan safely across runs.
Files you should care about
/config/state.json: baselines and checkpoints./config/state.manual.json: your manual adds/blocks.
If you back up CrossWatch, back up /config/.
Why state matters
Baselines stop CrossWatch from deleting “unknown” items.
Checkpoints help detect stale snapshots.
Guardrail files reduce flapping and retries.
When to reset state
Resetting can be useful when you change:
pair direction or mode
provider credentials
whitelists / library filters
Related:
Pair scoping: Scope
Safety model: Guardrails
State files are the orchestrator’s on-disk memory. They store baselines, checkpoints, and guardrail data.
This page lists what gets written, where it lives, and who uses it.
Base directory is CONFIG_BASE() (usually /config inside the container).
Code references:
cw_platform/orchestrator/_state_store.pycw_platform/orchestrator/_scope.pyplus each guardrail module (
_tombstones.py,_blackbox.py,_phantoms.py,_unresolved.py)
File location rules
Root vs .cw_state/
.cw_state/Core, user-facing state lives in
/config/Most “guardrail internals” live in
/config/.cw_state/
The orchestrator creates .cw_state automatically if missing.
Scope suffixes
Many files are scoped, meaning the filename includes a sanitized pair scope:
derived from env vars set by
_pairs._pair_env()created via
scoped_file(prefix, ext)
Typical scope strings:
one-way_plex-simkl_0two-way_plex-trakt_1health(temporary)
Scope length is capped (96 chars). Invalid characters are replaced with _.
Core files in /config/
/config//config/state.json
/config/state.jsonPurpose: canonical orchestrator state.
Written by: _state_store.save_state() (called at end of a feature run)
Read by: most runtime logic (baselines, checkpoints, policy merge, summaries)
Top-level shape (high-level):
Baselines
baseline.itemsis a dictcanonical_key -> minimal item“minimal item” is usually:
type/title/year/idsplus provider subobject (plex/jellyfin/trakt/simkl…) when needed
excludes transient fields
Baseline persistence filters out items if:
_cw_persist == false_cw_transient == true_cw_skip_persist == trueprovider subobject
ignored == true(provider-specific)
Checkpoints
stored at
providers.{PROVIDER}.{feature}.checkpointcheckpoint is whatever
module_checkpoint()returned (string-ish)
/config/state.manual.json
/config/state.manual.jsonPurpose: manual overrides. Merged into state.json at load time.
Read by: _state_store.load_state()
Written by: UI and API endpoints (not orchestrator core)
Contains:
manual adds: “always include”
manual blocks: “never sync”
Stored under the relevant provider+feature node (same providers nesting as state.json).
Manual blocks can be:
canonical keys (
imdb:tt...)ID tokens (
tmdb:123)title-year tokens (normalized)
/config/last_sync.json
/config/last_sync.jsonPurpose: last run summary for UI.
Written by: _pairs.py after a run
Read by: UI endpoints and logs views
Typical contents:
when the last run started/ended
per pair:
per feature counts (adds/removes/unresolved)
api totals (if recorded)
Not used for correctness; purely observability.
/config/watchlist_hide.json
/config/watchlist_hide.jsonPurpose: UI helper file to hide watchlist items.
Written by: UI
Cleared by: _pairs.py at end of a run
This is intentionally not a durable guardrail. It’s a UI affordance.
/config/ratings_changes.json
/config/ratings_changes.jsonPurpose: optional sink for rating-change traces.
Writer: some provider modules may append to it
Reader: UI/debug tools
Not required for orchestrator logic.
Guardrail internals in /config/.cw_state/
/config/.cw_state//config/.cw_state/tombstones.json
/config/.cw_state/tombstones.jsonPurpose: deletion memory. Used mainly by two-way.
Written by:
one-way: when removals succeed on destination
two-way: when removals succeed on either side
two-way: also when “observed deletions” are detected
Read by:
_tombstones.keys_for_feature(...)_pairs_blocklist.apply_blocklist(...)(indirectly, adds blocklist)
Key format:
{feature}:{PAIR}|{token}
Value includes:
atepochoptional
whystring
TTL:
sync.tombstone_ttl_daysdecides what is treated as active
Legacy migration:
/config/tombstones.jsonmay be migrated into.cw_state/tombstones.json
Blackbox files
Flap counters (*.flap.json)
Purpose: consecutive failure counters.
Filename pattern:
/config/.cw_state/{dst}_{feature}.{SCOPE}.flap.json
Written by: _blackbox.inc_flap(), record_attempts()
Read by: _blackbox.load_flap_map()
Blocked keys (*.blackbox.json)
Purpose: blocked keys with cooldown timestamps.
Filename pattern:
/config/.cw_state/{dst}_{feature}.{PAIR or SCOPE}.blackbox.json
Written by: _blackbox._promote()
Read by: _blackbox.load_blackbox_keys()
Pruned by:
_blackbox.prune_blackbox(cooldown_days=...)
Unresolved files
Pending unresolved (*.unresolved.pending.json)
Purpose: keys that failed apply this run.
Filename pattern (scoped):
/config/.cw_state/{dst}_{feature}.{SCOPE}.unresolved.pending.json
Written by: _unresolved.record_unresolved(...)
Contents:
Active unresolved (*.unresolved.json)
Purpose: active unresolved blocklist.
Filename pattern (scoped):
/config/.cw_state/{dst}_{feature}.{SCOPE}.unresolved.json
Read by: _unresolved.load_unresolved_keys(...)
The orchestrator writes *.unresolved.pending.json. The loader reads *.unresolved.json.
If you don’t promote pending → active, unresolved won’t fully block retries.
PhantomGuard files (watchlist)
Filename patterns (scoped):
/config/.cw_state/{feature}.{src}-{dst}.{SCOPE}.phantoms.json/config/.cw_state/{feature}.{src}-{dst}.{SCOPE}.last_success.json
Written by: _phantoms.PhantomGuard.record_attempt/record_success
Read by: _phantoms.PhantomGuard.load()
Purpose:
block repeated adds that don’t “stick” on destination
TTL:
cooldown_daysread from root configcfg["blackbox"]["cooldown_days"](yes, this is confusing)
Misc scoped helpers
anilist_watchlist_shadow.*.json
anilist_watchlist_shadow.*.json/config/.cw_state/anilist_watchlist_shadow.{SCOPE}.json
Written by:
_snapshots._maybe_backfill_anilist_shadow()
Purpose:
helps ANILIST entries map to better canonical keys by storing ANILIST IDs and matched source IDs.
Debugging cheat sheet
If a run seems “stuck” on the same items:
Check unresolved:
ls /config/.cw_state/*unresolved*see if pending is piling up without a promoted
*.unresolved.json
Check blackbox:
ls /config/.cw_state/*blackbox.jsonremove keys manually if needed
Check tombstones:
grep -n "watchlist:PLEX-SIMKL" /config/.cw_state/tombstones.json | head
Check phantom guard:
ls /config/.cw_state/watchlist.*.phantoms.json
Check baselines/checkpoints:
open
/config/state.jsonlook under
providers.{PROVIDER}.{feature}
Related docs
Last updated