diff --git a/Services/ClipLibrary.cs b/Services/ClipLibrary.cs
new file mode 100644
index 0000000..5cbe399
--- /dev/null
+++ b/Services/ClipLibrary.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace ytLive.Services;
+
+///
+/// TASK 65: where clips live and how many survive.
+///
+/// Not clip management. There is no gallery, no naming, no per-clip decision. A clip is
+/// written to clips\, the oldest is deleted when the next one lands, and the only knobs are the
+/// length (10 or 15 s) and how many to keep (3, 4 or 5). Anything richer belongs to a video editor.
+///
+public sealed class ClipLibrary
+{
+ public const string Extension = ".mp4";
+ public const string FilePrefix = "clip-";
+
+ private readonly Func _folder;
+
+ public ClipLibrary(Func folder) => _folder = folder;
+
+ /// Bytes the ring should hold for a length and bitrate, with 25% headroom for bitrate
+ /// movement. This is a memory cap, not a duration — the ring trims whole fragments.
+ public static long RingBytesFor(int seconds, int bitrateKbps)
+ => Math.Max(1, seconds) * (long)bitrateKbps * 1000 / 8 * 5 / 4;
+
+ /// Writes a clip and prunes to ; returns the file path, or null when
+ /// there was nothing to write (an empty snapshot must never become a zero-byte file).
+ public string? Save(byte[] mp4Bytes, DateTime nowLocal, int keep)
+ {
+ if (mp4Bytes.Length == 0) return null;
+ try
+ {
+ var folder = EnsureFolder();
+ var path = Path.Combine(folder, $"{FilePrefix}{nowLocal:yyyyMMdd-HHmmss}{Extension}");
+ var unique = path;
+ for (var n = 2; File.Exists(unique); n++)
+ unique = Path.Combine(folder, $"{FilePrefix}{nowLocal:yyyyMMdd-HHmmss}-{n}{Extension}");
+
+ File.WriteAllBytes(unique, mp4Bytes);
+ Prune(keep);
+ return unique;
+ }
+ catch (Exception ex)
+ {
+ Helpers.AppLog.Write(ex, "Clip save failed");
+ return null;
+ }
+ }
+
+ /// Newest first. The replay layer's playlist is this list, so its order matters.
+ public IReadOnlyList Recent()
+ {
+ try
+ {
+ var folder = EnsureFolder();
+ return Directory.EnumerateFiles(folder, FilePrefix + "*" + Extension)
+ .OrderByDescending(p => p, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+ catch (Exception ex)
+ {
+ Helpers.AppLog.Write(ex, "Clip list failed");
+ return Array.Empty();
+ }
+ }
+
+ /// Deletes everything past the retention count. Missing folders are not an error.
+ public int Prune(int keep)
+ {
+ if (keep < 1) keep = 1;
+ var removed = 0;
+ foreach (var stale in Recent().Skip(keep))
+ {
+ try
+ {
+ File.Delete(stale);
+ removed++;
+ }
+ catch (Exception ex)
+ {
+ Helpers.AppLog.Write(ex, $"Clip prune failed: {stale}");
+ }
+ }
+ return removed;
+ }
+
+ /// The folder beside the recordings, created on demand.
+ public string EnsureFolder()
+ {
+ var folder = Path.Combine(_folder(), "clips");
+ Directory.CreateDirectory(folder);
+ return folder;
+ }
+}
diff --git a/ytLive.Tests/ClipLibraryTests.cs b/ytLive.Tests/ClipLibraryTests.cs
new file mode 100644
index 0000000..5dadd30
--- /dev/null
+++ b/ytLive.Tests/ClipLibraryTests.cs
@@ -0,0 +1,76 @@
+using System;
+using System.IO;
+using System.Linq;
+using Xunit;
+using ytLive.Services;
+
+namespace ytLive.Tests;
+
+///
+/// TASK 65 slice 3 — clips on disk, with retention and nothing else.
+///
+/// The rule: write one file, delete the oldest past the count. No gallery, no prompt. The tests pin
+/// the two ways that could go wrong — a zero-byte clip from an empty ring, and pruning taking something
+/// that isn't ours.
+///
+public sealed class ClipLibraryTests : IDisposable
+{
+ private readonly string _root = Path.Combine(Path.GetTempPath(), $"ytLlive-clips-{Guid.NewGuid():N}");
+
+ public ClipLibraryTests() => Directory.CreateDirectory(_root);
+
+ public void Dispose()
+ {
+ try { Directory.Delete(_root, recursive: true); } catch { /* best effort */ }
+ }
+
+ private ClipLibrary Library() => new(() => _root);
+
+ [Fact]
+ public void SaveWritesIntoClipsBesideTheRecordings()
+ {
+ var path = Library().Save(new byte[] { 1, 2, 3 }, DateTime.Now, keep: 3);
+
+ Assert.NotNull(path);
+ Assert.EndsWith(".mp4", path);
+ Assert.Equal(Path.Combine(_root, "clips"), Path.GetDirectoryName(path));
+ Assert.True(File.Exists(path));
+ }
+
+ [Fact]
+ public void AnEmptySnapshotNeverBecomesAZeroByteFile()
+ {
+ Assert.Null(Library().Save(Array.Empty(), DateTime.Now, keep: 3));
+ Assert.False(Directory.Exists(Path.Combine(_root, "clips"))
+ && Directory.EnumerateFiles(Path.Combine(_root, "clips")).Any());
+ }
+
+ [Fact]
+ public void RetentionKeepsTheNewestAndDeletesTheRest()
+ {
+ var library = Library();
+ var folder = library.EnsureFolder();
+ for (var i = 0; i < 6; i++)
+ File.WriteAllText(Path.Combine(folder, $"clip-20261110-12000{i}.mp4"), "x");
+ File.WriteAllText(Path.Combine(folder, "someone-elses-file.mp4"), "keep me");
+
+ var removed = library.Prune(keep: 3);
+
+ Assert.Equal(3, removed);
+ var remaining = library.Recent();
+ Assert.Equal(3, remaining.Count);
+ Assert.Contains("120005", remaining[0]); // newest first
+ Assert.True(File.Exists(Path.Combine(folder, "someone-elses-file.mp4")));
+ }
+
+ [Fact]
+ public void RingBytesFollowLengthAndBitrate_WithHeadroom()
+ {
+ var tenAt8 = ClipLibrary.RingBytesFor(10, 8000);
+ var fifteenAt8 = ClipLibrary.RingBytesFor(15, 8000);
+
+ Assert.True(tenAt8 > 10L * 8000 * 1000 / 8); // more than the exact wire size
+ Assert.True(fifteenAt8 > tenAt8);
+ Assert.True(tenAt8 < 15L * 1024 * 1024); // and still modest
+ }
+}