commit dcc6501471c9d7f0cc6c65b79cd9df408732b774
parent fc175654d01b10966ba45fa5933e39f5b6c3b742
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 10 Oct 2026 00:43:16 -0400
plans: release 21 D2, as shipped (the saved-container tier for clip windows)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 63 insertions(+), 0 deletions(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- **A clip window of a video whose source is saved is cut from it, not fetched.** `fetch_clip`, `POST /api/media/fetch-window` and `pnpm ops fetch-windows` now cut a window out of the video's saved container (a persisted source, a full-source fetch, or media attached from a local archive) when it covers the seconds asked for, and answer at once as a cached window — no request to the platform, so a deleted channel's held videos are clippable. The window's sidecar records `source: "saved-video"`, and the video page marks it "cut from the saved video". A batch runs such windows as their own job on `clips:saved-video`, outside every platform's queue, hold and cooldown. A saved video whose file cannot be read (its drive unplugged, the file gone) is refused with the media guard's sentence rather than fetched; one that ends before the window is fetched as before.
- **An X fetch with a `limit` stops at that many posts.** "Fetch posts" with `limit` (`pnpm ops fetch-posts {"limit": 400}`) on a gallery-dl X channel read the whole history instead — a new channel walked 3,803 posts under the rate limit and held the platform queue for hours — because the cap counted media files, which a metadata-only read has almost none of. It now caps the posts themselves.
- **A home seeder of last resort, behind a VPN.** `archilyzer seed` seeds the playable torrents of the sites named in the new `settings.seeder` (`sites`, `trackers`, `maxUploadKiBps`, `maxConnections`, `pollSeconds`, `standbyAfterSeconds`, `bindInterface`; SETTINGS.md) to desktop clients over TCP and to browsers over WebRTC — but each torrent only while no other seeder has it: other seeders seen on every poll for `standbyAfterSeconds` puts that torrent on standby (it stops announcing and closes its peers, keeping the data), and it comes back at once when a leecher is waiting with no other source, or after the same window with no other seeder. Every change is logged with its reason. No DHT, no local discovery, no UPnP. `archilyzer tracker` is a self-hosted HTTP + WebSocket tracker that tracks only those torrents. `docker-compose.seeder.yml` (profile `seeder`) runs both inside a WireGuard container's network namespace (gluetun, its firewall always on), so a tunnel that is down means no network, never the home connection; the WireGuard config is yours (`SEEDER_WG_CONF`, required, mounted read-only). `archilyzer doctor` compares the seeder's egress address with the host's and fails when they are the same; it says "seeder not configured" until `seeder.sites` names a site.
- **Saved videos can be made browser-playable, with a torrent each.** `pnpm ops prepare-playable` (`POST /api/ops/prepare-playable`) and `archilyzer media playable <slug>` remux each of a channel's saved containers — without re-encoding (`-c copy`) — into an mp4 with its index in front, or a webm when it already is one (VP9/AV1 with Opus), drop subtitles, metadata and chapters, and make one single-file torrent of the copy: named `<id>.<ext>`, no web seed, no comment, no "created by", 256 KiB–1 MiB pieces. They go to `playable/<slug>/<id>/` beside the saved-video store, listed in `playable/<slug>/playable.json` with each infohash. `"trackers"` is the announce list written into each torrent (none by default); it is not part of the infohash, so the same torrent can be announced elsewhere later. A video already prepared from the same source (by sha256) is skipped, so a re-run is a no-op; a codec a browser cannot play without re-encoding (HEVC, MPEG-4 Part 2) is listed and left alone.
diff --git a/plans/release-21.md b/plans/release-21.md
@@ -299,6 +299,68 @@ the three new records is the operator's (`transcribe-one`), as ruled. D2, D4, D5
`test:scripts` 708 + 3 skipped, four builds, the full editor suite (start mode) green after re-running three load
flakes alone. The pilot below waits for D2.
+#### D2, as shipped — clip windows cut from a saved container
+
+Release 21's D2, on `r20/d2-r20` off `r20/integration` (`585be292`), overnight 2026-10-10 (Track D, part 1).
+
+**What it does.** `common/lib/savedVideoWindow-server.ts`: `savedWindowSource` reads the video's saved-video
+pointer (any pointer — a persist, a `full: true` fetch, a D1 `local-archive` attach), stats the container through
+the drive watchdog (`onDrive`) and probes it once (`probeMedia`), answering `none` (no pointer), `unreadable`,
+`not-covering` (the window ends past the container's duration, to `WIN_EPS`) or `covers`; `cutSavedVideoWindow`
+cuts with evidenceClip-server's `evidenceCutArgs` (input-seeked, H.264 + AAC mp4 fitted inside 1280×720; a container
+with no picture is cut as sound) to a temp name, renames it to `clips/<from>-<to>.mp4`, and writes the fetched
+sidecar shape (`requestedBy`, `manifest`, `clipId`, `reason`, `requestedAt`, `pad`, `bytes`, `fetchedAt` = when it
+was cut) plus `source: "saved-video"` — a new optional `ClipProvenance.source` that `parseClipProvenance` reads;
+absent on every fetched window.
+- **Single** (`fetchWindowAction`): the clip cache, then the in-flight jobs (A5), then the saved container, then
+ the network. `covers` → the channel's text guard (`assertChannelTextReadable`, which a `fetch-window` job's
+ `needsText` would have asked), the cut, `cached: true` with the window's probed `height`, no job. `unreadable` →
+ 503 with the media guard's sentence (`Channel "<slug>": media is not reachable — its saved video <path> is not
+ there (drive not mounted?)`, or `… could not be read by ffprobe`, or the watchdog's `drive not answering …`), or
+ the store guard's (`SavedVideosStoreInTransitionError`) when the file is missing while the store's move marker
+ is present. `none` and `not-covering` → the network path, unchanged. A failed cut → 500 with ffmpeg's last lines.
+- **Batch** (`fetchWindowsAction` → `controller/fetchWindows.ts`): after the cache and the in-flight check, a
+ covered window joins one group of its own — platform `saved-video` on `clips:saved-video`
+ (`SAVED_VIDEO_CLIP_QUEUE` / `SAVED_VIDEO_CLIP_PLATFORM`, `lib/queueKeys.ts`), which `groupRefusal` does not
+ ask about a hold or cooldown; an unreadable one is `unresolved` with the guard's sentence; a not-covering one joins
+ its platform's group. The controller asks the same tier for every item before the URL and the gap: a cut costs no
+ pause and is not a network attempt (`result.cut`, logged `✂`, and "N cut from saved videos" in the summary);
+ `unreadable` fails the item as `unreachable`; a failed cut is the new class `cut-failed` and the run carries on.
+ A5's dedupe covers the new queue unchanged: a queued `saved-video` job's items are in its spec.
+- The video page's fetched-windows card adds "· cut from the saved video" to such a window. `mcp/src/instructions.ts`
+ (the clip step): "A video the archive holds locally is cut from its saved copy without a fetch (cached at once)."
+
+**Rulings applied.** No pointer → the network exactly as before; a pointer whose container is unreadable → refused,
+never a fall-through; a container that does not cover the window → the network (orchestrator, 2026-10-10).
+
+**Found and left.**
+- A local cut is at most 1280×720 (evidenceCutArgs' box), whatever `maxHeight` asks; the answer carries the window's
+ `height`, as a cached window's does. A smaller `maxHeight` gets a cut taller than it asked for.
+- `ops-api.spec` cannot reach a cut: the e2e server runs with `FFMPEG_BIN`/`FFPROBE_BIN` set to the fixture fakes
+ (`editor/package.json` `dev:test`/`start:test`), whose ffprobe answers a bare duration, never `-of json`, so every
+ saved container there probes as unreadable. The nearest tests run the real thing instead: the route test cuts
+ over HTTP with real ffmpeg, and the action test runs the `saved-video` batch job through `runManagedFunction` to
+ `done` (below). No e2e case was added.
+
+**Commits**
+
+| Commit | What |
+|---|---|
+| `11753809` | `channels:` release 21 D2 — the saved-container tier in the single and batch window paths, `ClipProvenance.source`, the `clips:saved-video` queue, the card's marker, the MCP line; 18 tests |
+
+**Gates.** `pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit` clean. New tests, all real ffmpeg over
+8 s lavfi containers in temp dirs: `lib/savedVideoWindow-server.test.ts` 9 (no pointer; not covering; a missing
+container; an unmounted store dir; the store mid-move; a file ffprobe cannot read; the cut and its sidecar, read
+back by the clip cache; sound only; a failing ffmpeg leaves nothing), `controller/fetchWindows.test.ts` +2 (cut /
+unreadable / not-covering in one run with no request and no pause; a failed cut), `app/api/media/fetch-window/
+route.saved.test.ts` 4 (a covered window → 200 `cached: true`, `provenance.source` `saved-video`, height 90, no job,
+then a clip-cache hit; past the container's end → the network door, a YouTube cooldown's 409; a missing container
+→ 503 with the guard's sentence; no pointer → the network door), `app/channels/[slug]/videos/
+fetchWindowsAction.test.ts` 3 (the dry run's groups and unresolved; a real run whose `saved-video` job ends `done`
+with the window and sidecar on disk while the cooling YouTube group is refused; A5's dedupe on `clips:saved-video`).
+Editor unit 225/225; the touched common files (clipWindow, evidenceClip, savedVideo*, fetchWindows, windowJobs)
+61/61; architecture 6/6; mcp 293/293. The whole common suite runs once at the end of the track (part 2).
+
## Rollout (pilot)
1. `pnpm ops attach-media --json '{"slug":"the-incredible-salt-mine","source":"<archive>/Uploads/YouTube.zip","dryRun":true}'`.