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.