# CURRENT.md — session state Last updated: 2026-09-16 This file tracks the work state. Read it first. Update it before you stop. ## Current focus (broad) The text layer is done. Voice actions and audio work. The local LLM harness (Veskslop agent, Go + Eino) is scaffolded and smoke-tested; the daemon-side task push is next. The web UI has a first mobile draft at 390px. ## In progress - Media archive project, branch `media-scrape-project` on `veskslop-t`. Design approved 2026-09-16 (docs: [media.md](docs/media.md), [media-scraper.md](docs/media-scraper.md), [slopbot.md](docs/slopbot.md), [media-mcp.md](docs/media-mcp.md)). Decisions: scrape the channel the command is sent in; slopbot (discord.js bot core) for guild channels and the puppet core bridge for group DMs; one self-contained SQLite + media dir per scrape under `media-scrape/db/` (gitignored); links indexed by default, downloads optional, third-party embeds never downloaded; 30 GiB default limit with a 90% warning and 5% no-delete overrun, usage bar and clean button in Settings → Bot; Media and Bot top-level tabs; MCP hooks designed for C8. The slopbot scaffold (config loader, invite helper) and its discord.js dependency were added 2026-09-16. C5 smoke test: `/api/media/status` and `/api/media` answer on a daemon with no core. Checkpoints, push after each: - [x] C1 design docs, AGENTS/INDEX pointers, `.gitignore` - [x] C2 core message shape: embeds, forwards, attachment metadata - [x] C3 media-scrape store and puppet walker, tests - [x] C4 slopbot backend, tests - [x] C5 daemon wiring: `scrape media` command, MediaService, `/api/media/*` - [x] C6 Media and Bot tabs in the Web UI - [x] C7a storage limits, retention, warning/confirm flow, storage and clean API - [x] C7b Settings group: limit field with info hover, usage bar, clean modal - [x] C8 MCP service hooks (`scrape_media`, scrape reads, storage, clean; reads in `run_commands`) - [x] C10 downloader: attachment and sticker bytes, hash-addressed, serial, size-capped, retries; embeds excluded in SQL; local files served at `/api/media///file` and preferred by the Media tab. Live run 2026-09-16: scrape `...-20260916083401` fetched the 784 KB attachment; storage counted actual bytes. - [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`: `previewed: 2`; thumbs served over `/api/media///thumb` (200). - [x] C44 One traffic pipeline and split ETAs. Walks and backfill batches now share a media-traffic lock: a walk holds it for its whole run, a batch for one batch, so the pacing settings bound total traffic instead of each action. Starting then immediately stopping a scrape now fetches nothing (tested). Backfill ETAs split into `etaStartMs` (waiting behind earlier jobs), `etaRunMs` (own work), and `etaTotalMs`; the Bot tab shows only the total and puts `starts in X \u00b7 runs for Y` in the tooltip, same for the queue totals. Tests: lock ordering, stop semantics, ETA ordering. - [x] C43 Bot-tab backfill control room and media-tab strip rules. The engine gained `previewBatch`/`rateLimitStatus`/`updateOptions`; a new `BackfillQueue` groups preview work per chat (paused and resumed scrapes of a chat combine), runs one job at a time in batches, and reports pending/cached/failed per kind, throughput, measured and settings-based ETA, and the live cooldown. `MediaService` drives it and exposes `GET /api/media/backfill` plus start/pause; `/api/preferences` now carries `media.tunables` (Backfilling / Scraping / Timers, persisted and applied live). The Bot tab shows totals with a Bot/Puppet split, per-chat job cards with progress and Start/Pause, **Start backfill** / **Pause all**, the red `API timed out, increase timers` banner, and the three settings groups. The media strip shows running plus one paused scrape, dismisses a paused one on Stop without stopping it, turns red while limited, and shows a session-only `N scrapes completed` note. The old `VESKSLOP_MEDIA_*` tuning env vars are gone. New unit tests cover batching, the cooldown, live retuning, and queue grouping/finish/pause; the Playwright smoke test covers the Bot tab. Also fixed a real stall the queue exposed: `pendingPreviews` never returned thumbnail-less embeds, so they sat pending forever and stalled the job; the store now includes them and the resolver handles Discord CDN unfurls through the proxy frame, with a content-type guard so the og:image fallback never reads media bytes. Live: daemon restarted, pending falling (6018 → ~5900), cached rising, dead CDN unfurls closed as 404, jobs queue smallest-first, settings round-trip verified (21.4 → 37.5 rows/min at 30 s cooldown). - [x] C42 Preview attempts cap. The restart log showed the backfill retrying dead hosts (Gfycat DNS is gone, Streamable answers 400) forever as "transient". Rows now carry `preview_attempts`; after `VESKSLOP_MEDIA_PREVIEW_MAX_ATTEMPTS` (3) the row is marked `unreachable after N attempt(s)` and leaves the pending queue. Unit-tested. - [x] C41 Restart keeps its logs. A button restart spawned the replacement with `stdio: ignore`, so anything after the restart vanished. The replacement now appends to `daemon.log` beside the token file (`VESKSLOP_DAEMON_LOG` overrides); documented in [docs/daemon.md](docs/daemon.md). - [x] C40 Stale scrape states. Two `general_chut` scrapes showed `running` because the daemons that owned them were killed mid-job; the state file kept the last written value. On daemon start the engine now reconciles `running`/`queued` scrapes to `paused`, and History has a per-scrape **Delete** button (clean with a single ID) next to Resume. Unit-tested. - [x] C39 File-type filter and the 4chan default. The Advanced drawer gained extension chips (video mp4/webm/mov/mkv, image png/jpg/gif/webp/avif, audio mp3/ogg/wav/m4a/flac) matched on URL or content type, plus a default-on **Hide 4chan links (usually dead)** toggle. Backend, API (`ext`, `excludeHosts`) and MCP support; live: video count 7696 → 7383 with the default exclusion, `ext=mp4`/`webm` return only those files. Also confirmed the mr-game-memorial group has 924 attachment videos with 772 previews cached and 0 preview errors — the rest are just pending in the 2,140-item backfill queue. - [x] C38 Filter layout swap. The common filters (filetype chips, source kind, server/DM origin, multi-select People popover) now sit in the always-visible bar; the Advanced search drawer holds aspect, resolution, channel, date range, cached-only, and the card-size slider. Apply/Reset stay in the bar. Fixed a dropped `defaultFilters` helper found by the Playwright smoke test, which passes on the new layout. - [x] C37 Advanced search drawer, adaptive paging, and zoom. Filters moved into an **Advanced search** drawer with Apply/Reset (no auto-update): filetype chips, source kind, origin (server/DM), multi-select People, and an Advanced section (aspect, resolution, channel, dates, **cachedOnly**). A **Card size** slider scales tiles from 100 px (2/3 of current) to one image per row; page size = full rows about 1.5 screens tall and follows zoom/resize. Infinite scroll preloads the next page's JSON and previews. Backend gained `authorIds`, `origin`, `cachedOnly` on `/api/media`, `/api/media/stats`, and MCP `query_media`. Verified: `origin=dm` and `cachedOnly=1` return the expected rows. - [x] C36 Daemon restart, preview diagnostics, browse-time caching, and rate-limit pauses. Settings → Server has a **Restart daemon** button (`POST /api/daemon/restart`: re-execs the daemon, keeps the client running). The preview pass logs every failure with scrape/item/kind/ provider/reason plus a per-pass summary, and `GET /api/media/preview-failures` lists them. Browsing no longer fetches Discord at all: the grid uses cached stills only (placeholders for missing ones), GIF hover plays the cached animated GIF (kept as `anim_hash`, served from `/anim`), and streaming only starts on explicit play. `429`s from previews/downloads raise a cooldown (`VESKSLOP_MEDIA_RATELIMIT_PAUSE_MS`, default 60 s, or `Retry-After`), leaving rows pending and pausing both walker and previews. - [x] C35 Walk/preview cycling. A live scrape now alternates: walk 5 pages, preview up to one batch (25, with the cooldown), walk on. Tiles fill in during a long walk instead of only at the end, and preview traffic is spread through the walk. Configurable with `VESKSLOP_MEDIA_WALK_CYCLE_PAGES` (0 defers previews to the end). The final pass still drains whatever remains. - [x] C34 Resume from cursor. A paused scrape can continue where it stopped instead of re-walking the top: `POST /api/media/scrape/resume`, MCP `resume_media_scrape`, and a Resume button in History. Previews never needed the walk — backfill fetches them straight from the stored URLs; the walk is only for discovery. Live: the paused `gernal_chut` scrape resumed at cursor `365256427247239181`, fetched its final page, and entered the preview pass. - [x] C33 Puppet history pagination. The core's `message.fetch` read the client store, which keeps only the newest messages plus the channel's first — every page was 10 messages and the walk stopped after one page (a 10-message, 4-media scrape of an ancient group with thousands of messages). It now fetches through the client's REST layer first (contiguous 100-message pages, `transport: "rest"`), store as fallback. Rebuilt and restarted the managed client; live re-run of channel `591000745164406784`: 21+ pages / 2,100+ messages and climbing, with hundreds of images/videos being indexed. - [x] C32 Group-chat clarity and puppet refresh. Group DM scrapes already worked (live-verified against the running daemon: `zıbb & fred` and a 1:1 DM, backend `puppet`, done, previews cached); the Group chat tab had shown an empty list while the core was offline. It now says so explicitly and offers Retry. Expired attachment URLs in DM/group-DM scrapes refresh through the core bridge (`message.fetch` with `before: id+1`, which returns exactly the target). - [x] C31 Batched previews. The preview pass now runs in batches (default 25) with a cooldown (default 60 s), configurable via `VESKSLOP_MEDIA_PREVIEW_BATCH` and `VESKSLOP_MEDIA_PREVIEW_BATCH_PAUSE_MS`; batching counts rows, not successes. Live log: `preview batch of 25 done, pausing 60000 ms`. - [x] C30 Preview completeness. Audio attachments and voice messages get an ffmpeg waveform strip; oversized animated GIFs fall back to an ffmpeg-read first frame instead of `too large`; transient network failures stay pending for the next backfill instead of becoming permanent. Live: a scrape with 58 audio items backfilled 13 waveforms in 40 s, serving `image/webp`. Remaining non-cached: Lottie stickers/custom emoji (not indexed as media) and embeds whose pages refuse bots (marked PRIVATE/GONE by their 403/404). - [x] C29 Attachment video previews. Discord's image proxy renders a frame for video files (`?width=320&height=320&format=webp`), so `previewUrlFor` now resizes attachment mp4/webm in links mode too and `pendingPreviews` includes non-embed videos. Live: paused-scrape videos backfilled to `image/webp` thumbs, pending queue drained to 0. - [x] C28 Media grid usability. Infinite scroll (240 px threshold) with a Load more fallback, `previewError` exposed so tiles show PRIVATE (403) or GONE (404/410) with the reason in the tooltip, and the scrape strip lists only queued/running jobs — paused scrapes move to History. - [x] C27 Non-media stream guard. Rows awaiting backfill fell back to first-frame `