Skip to content

Governed TV Episode Acquisition Lifecycle

This capability family extends canonical read-only TV episode inventory and torrent candidate discovery through proposal creation, Lich review, bounded qBittorrent submission, a separately approved download start, durable progress observation, and deterministic completion stop. It is TV-episode-only. The current lifecycle stops before any filesystem or Plex handling.

Flow and immutable identity

An explicit series and aired SxxExx coordinate resolves through the existing TVmaze/Plex canonical identity path. Plex absence, a unique library scope, one eligible normalized candidate, and consistent ranking evidence are mandatory. Ambiguous identity, inconsistent inventory, an already-present episode, or no acceptable candidate fails closed before durable state is written.

Provider APIs and hostile metadata remain behind adapters. Candidate locators must already have passed the bounded magnet parser. The durable proposal binds the canonical episode key, candidate digest and info hash, title, quality, size, provider, candidate-set digest, ranking evidence, protected locator, and exact submit_approved_episode_torrent capability. Review and normal rendering omit the full locator. Any change requires a new proposal and review.

Proposal, approval, and explicit invocation

EpisodeAcquisitionProposalService and EpisodeAcquisitionExecutionService reuse GovernedProposal, SQLiteGovernedActionStore, Zeus-compatible bounded evidence, and LichApprovalService. Proposal creation has no downloader client. Lich Approve and Decline controls record review state only: approval does not invoke the executor or create a receipt.

The separate exact command Execute approved acquisition proposal <proposal_id> reaches the same transport-neutral runtime service from every transport. Short conversational assent and arbitrary magnet text cannot select this capability. Rubick registers the executor as Lich-required and explicitly not auto_execute.

Live execution gates

Proposal-time readiness is observational and never authorizes execution. The executor immediately re-checks Gluetun's live running observation, the current canonical qBittorrent-to-Gluetun namespace binding, and absence of an independent qBittorrent Docker network. Cached “last healthy” state is insufficient.

The executor also checks proposal and approval expiry, lifecycle eligibility, immutable proposal integrity, canonical episode identity, candidate digest, info hash, protected magnet, quality and size binding, exact capability, and the Lich approval's exact proposal hash and candidate binding. Missing, stale, or ambiguous evidence fails closed.

Mutation boundary, idempotency, and receipt

The typed qBittorrent adapter can only inspect one exact info hash and add the proposal-bound magnet stopped, non-auto-managed, and non-force-started. It exposes no arbitrary URL, delete, resume, reannounce, scheduler, category, path, or general qBittorrent API surface. Stopped initial state is the bounded choice because submission and download start are distinct lifecycle actions.

The transition to execution_in_progress uses durable optimistic revision enforcement as its single-flight boundary. An already verified submitted proposal returns its existing receipt. If the exact approved hash already exists safely, no duplicate add is issued and the receipt records an idempotent existing-hash result. An uncertain add or unverifiable identity is recorded as bounded Zeus failure evidence and execution_failed; blind retry is refused until an explicitly governed reconciliation slice exists.

Only after exact hash and stopped/non-managed state verification does the service persist bounded live VPN, containment, proposal, approval, media, candidate, qBittorrent, timestamp, and executor-version evidence; persist the action receipt; and transition the proposal to submitted. Tracker metadata, credentials, and the protected magnet are omitted from execution evidence.

Separate download-start authority

The submission approval authorizes only submit_approved_episode_torrent; its machine payload cannot authorize a later start. An operator must explicitly request a second Lich review with Request download start for submitted acquisition proposal <proposal_id>. That review binds the immutable proposal hash, submission receipt, candidate digest, exact info hash, and the typed start_submitted_episode_download capability. Approval still performs no mutation. A separate Start submitted episode download proposal <proposal_id> invocation is required.

Immediately before ordinary start, the lifecycle service verifies the durable submitted proposal and receipt/evidence chain, exact safe stopped torrent, non-auto-managed and non-force-started state, current Gluetun running readiness, canonical namespace containment, absence of an independent network, and current exact Lich authority. It refuses already-complete torrents. The adapter exposes only exact-hash inspection, ordinary start, and stop; it has no arbitrary hash input from conversational text, force-start, delete, reannounce, category, path, or auto-management operation.

Durable monitoring and VPN loss

Observe submitted episode download proposal <proposal_id> resolves the exact hash from durable proposal and submission receipt identity, so process or transport restart does not depend on conversational memory. Each tick reads bounded qBittorrent fields: hash, state, progress, downloaded and total bytes, remaining bytes, download/upload speed, management flags, condition, and UTC timestamp. It writes bounded observation evidence. Provider names and torrent text are data only and cannot select an action.

Episode downloads are the first consumer of the transport-neutral Io durable tether primitive. Clockwerk governs time and Io governs continuity: every 60 seconds Clockwerk pulses the registered episode watch class, and Io relinks a bounded batch of durable proposals whose exact state is download_active before invoking the existing typed EpisodeDownloadLifecycleService.observe(proposal_id) path. Sixty seconds is conservative for torrent progress: it provides useful operational visibility without aggressive provider polling. Configuration remains bounded to 30–3,600 seconds and 1–100 proposals per tick.

Eligibility is durable-state-based. Proposal-only, submitted but unstarted, start-review, start-in-progress, download_safety_stopped, failed, expired, and download_complete records are excluded. Conversational state, torrent names, and provider-wide enumeration are never used. Proposal and receipt-bound exact hash identity prevent unrelated torrents from entering the watch set.

One Io-owned non-blocking single-flight lock suppresses overlapping pulses. Restart creates one new process registration that relinks the same durable active records; it neither repeats start nor creates per-request schedulers. If the torrent completed while Luna was offline, the next tick uses the existing idempotent completion reconciliation and exact stop path. Terminal transition removes the record from later watch sets.

Io calls only the registered typed observe; it has no direct qBittorrent adapter and no start, resume, force-start, delete, reannounce, category, path, arbitrary stop, or auto-management authority. Its bounded status reports monitoring activity, proposal identity, lifecycle state, last observation time and progress, cadence, and expected next observation time without torrent names or paths.

Containment remains the primary fail-closed network boundary. Every active tick also checks live Gluetun readiness and canonical containment. An unhealthy or ambiguous result is recorded and invokes only the separately approved exact safety stop. Existing VPN supervision remains the sole recovery controller. The lifecycle never routes around Gluetun, gives qBittorrent an independent network, or automatically resumes after recovery. A stopped download requires a new explicit governed authority decision before any future resume policy. download_safety_stopped breaks the Io tether, so VPN recovery cannot silently resume or continually operate on the torrent.

Deterministic completion and stop

Completion requires mutually consistent qBittorrent structured state: positive total size, zero amount left, downloaded bytes at least total bytes, progress of one, and a complete/upload-side state. Filenames and conversational claims are never completion evidence. If qBittorrent briefly uploads, the next bounded tick stops only the exact governed hash. The service then verifies stopped, non-auto-managed, non-force-started state and zero active transfer, persists completion evidence and receipt, and transitions durably to download_complete. Repeated start and completion calls reuse durable records without duplicate provider mutation or receipts.

Promotion-readiness evidence

After download_complete, PromotionReadinessService may inspect the exact receipt-bound torrent through qBittorrent's read-only exact-hash properties and file endpoints. The configured completed root defaults to /media/plex/downloads/complete, matching the Compose mount. Provider paths must be normalized relative paths beneath that root. Absolute paths, traversal, symlinks in the resolved chain, special objects, multi-link regular files, duplicate paths, incomplete entries, changing files, unexpected extensions, and bounded-size violations produce immutable blocked evidence.

Policy version episode-promotion-readiness-v1 permits exactly one .mkv, .mp4, or .m4v media file plus bounded subtitle or metadata sidecars. It allows at most 64 files, 50 GiB per file, and 100 GiB total. Every accepted file is SHA-256 hashed and re-statted; the ordered manifest binds policy, canonical episode identity, byte sizes, hashes, and an advisory destination name. Readiness evidence is terminal and replayed after restart without another provider or filesystem access. A separate Io tether enrolls durable download_complete records lacking terminal evidence and uses the existing bounded single-flight observation mechanism.

The operator surface is Inspect promotion readiness for <proposal_id>. Output omits absolute source paths and provider locators. ready means only that immutable evidence passed the current read-only policy. It is not a Lich approval, promotion proposal, execution receipt, Plex visibility claim, or authority to mutate qBittorrent, files, or Plex.

Explicitly deferred

This slice does not stage files, inspect media with ffprobe, rename or move media, refresh or promote into Plex, delete or clean up torrents or files, implement general seeding ratio/time policy, schedule recurring episodes, or rotate credentials. It stops after verified download completion, exact stopped-state handling, and immutable promotion-readiness evidence. Promotion proposal construction remains a later governance slice.