54 KiB
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-projectonveskslop-t. Design approved 2026-09-16 (docs: media.md, media-scraper.md, slopbot.md, 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 undermedia-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/statusand/api/mediaanswer on a daemon with no core.Checkpoints, push after each:
- C1 design docs, AGENTS/INDEX pointers,
.gitignore - C2 core message shape: embeds, forwards, attachment metadata
- C3 media-scrape store and puppet walker, tests
- C4 slopbot backend, tests
- C5 daemon wiring:
scrape mediacommand, MediaService,/api/media/* - C6 Media and Bot tabs in the Web UI
- C7a storage limits, retention, warning/confirm flow, storage and clean API
- C7b Settings group: limit field with info hover, usage bar, clean modal
- C8 MCP service hooks (
scrape_media, scrape reads, storage, clean; reads inrun_commands) - C10 downloader: attachment and sticker bytes, hash-addressed, serial, size-capped, retries;
embeds excluded in SQL; local files served at
/api/media/<scrape>/<item>/fileand preferred by the Media tab. Live run 2026-09-16: scrape...-20260916083401fetched the 784 KB attachment; storage counted actual bytes. - 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/<scrape>/<item>/thumb(200). - C59 Scrape names and the completeness check. Scrape listings (History, Clean media,
Bot-tab job/lane cards) now lead with the first two words of the channel or group-chat name
plus the scrape id. New
poc/scripts/verify-scrapes.ts(pnpm verify:scrapes) checks every index: done state, no lastError, messages non-zero, messages vs the Discord search total, and the oldest message against the source's snowflake creation time (60-day grace). Scrapes a later full walk supersedes are reported asold, not failures. Result: 10 complete, 4 superseded, 1 genuinely incomplete — the early DM test scrape805255536223715370-20260916215657(10 messages, pagination stopped before the fix, never re-scraped). - C58 Titles from Discord's embed cache. Gone YouTube videos have no oEmbed answer, but
Discord keeps the embed title (and, with the rebuilt core bundle, the author/channel). The
title lane now asks the source message's embed first and only then the provider oEmbed; the
core maps
authorNamefromembed.author.name, extraction stores it astitle_author, and the viewer shows a prominent title/channel heading plus "Video unavailable (removed or private)" when a YouTube row has neither. The 783 title errors were reset and the lane backfilled them: the reported example (message 703788394337009754) now reads "Gun Test: Firing Empty Shell's Handheld XM556 Microgun". The channel name populates after the Vesktop client restarts on the new bundle (the client is externally started). - C57 Page mode. The media grid has an Infinite/Page flippy toggle in the bar. Page mode
renders one
pageSizeslice viaoffset = (page-1)*pageSize, shows Prev / editable page number / total / Next, persists the page in localStorage, and scrolls to the next page one at a time with a 2 s cooldown after each load. Same filter + page is stable while the archive is unchanged. Infinite mode keeps the stream buffer; prefetch, buffer fill, and the buffer are disabled in page mode. Page mode pre-loads the next page's JSON and preview images, so jumping one page renders instantly. - C56 Recovery completed. First run after the C55 fix recovered 1,117 of 1,240 Discord-CDN
previews (90%); the last 123 were confirmed genuinely gone (refresh + direct fetch and
/streamboth fail) and are markedunavailable after recoveryso later runs skip them. The recovery query now only covers HTTP errors on refreshable URLs, so proxyno preview sourcerows are not swept. Archive: 26,725 cached previews, 7 pending, 1,621 terminal provider failures, and the recovery task reports 0 to retry. - C55 Recovery fix. The first recovery run parked live files because refreshed embed URLs
land in
play_urlwhile preview resolution kept using the staleurl; the engine now resolves embed previews fromplay_urlfirst and re-resolves from the refreshed URL on retry, and both refresh branches canonicalizeurlto the direct media link. The task also counts rows that park as suspects asdeferred(not gone), tries each row once per run instead of looping, and initializes the slopbot so guild scrapes refresh through the bot. The 192 suspects from the broken run were converted back to unverified so they are retried. - C54 Message links, suspect retries, and preview recovery. Items now carry
messageUrl(frommedia_sources+ scrape guild), the viewer shows/copies it instead of the expiring CDN link and opens a fresh stream for View full, YouTube items offer an Open on YouTube fallback, and the metadata gained Channel/Scrape/Title (title includes the channel kept from oEmbed,title_author). Preview and download HTTP failures on Discord hosts first refresh the URL from the source message and retry; if that fails the row parks as a suspect with one delayed retry (50 s, runs after the batch; a batch two-thirds suspect doubles the cooldown, 60 s floor). Still-failing rows are unverified and skipped by normal passes; the Bot tab shows suspect/unverified counts and a segmented progress bar (cached blue, pending gray, failed red). Recover Discord previews is a paused one-shot task that re-fetches every Discord-CDN row closed with an HTTP error across all scrapes (mixed bot/puppet via the existing refresh path) and lands in the task history when done. All 1,208 Discord 404 rows sampled 15/15 recoverable. - C53 Title catch-up as its own lane. Titles no longer share the preview batch: a dedicated
TitleLanewalks every scrape with a pending title, 100 rows per batch with a 10 s pause (hard-coded; it is a one-time catch-up and future titles arrive with the scrape), and takes no media-traffic lock because it only calls YouTube's oEmbed. NewtitleBatchon the engine, per-scrapetitlePending/titled/titleFailedcounters,/api/media/titles+ start/pause routes, and a Bot-tab card with progress, throughput, ETA, and Start/Pause. The lane auto-starts on boot; with nothing pending it exits immediately. Preview backfill goes back to previews only. Unit tests fortitleBatchand a lane catch-up run; smoke covers the card. - C52 Wildcard title search.
*now maps to a LIKE wildcard in both title modes: a leading*drops the word anchor (*carfinds scar and discard), in-token stars match between letters (c*r*), and a trailing star matches the plain prefix.%and_are escaped with anESCAPEclause so they only match themselves. Unit tests cover prefix vs wildcard vs in-word matches; the drawer hint documents the syntax. - C51 YouTube Shorts normalization. Shorts links broke the embedded player and View full
because the viewer needs
watch?v=. Extraction now rewritesyoutube.com/shorts/<id>(andm.) to the watch form, the viewer also accepts/shorts/<id>defensively, and a one-timePRAGMA user_versionmigration rewrites existing rows. The live archive had 45 shorts rows; one collided with an already indexed watch URL and folded into it (preview fields merged, message links moved) — the migration is idempotent and unit-tested, including the collision. 45 → 0 shorts rows, and the user's exampleuAf18d7mrnUis nowwatch?v=uAf18d7mrnUwithembed/uAf18d7mrnUas the play URL. - C50 Title backfill in the queue. YouTube-like rows (provider or URL matching
youtube/youtu.be) without a title are now a second work type in the backfill queue: resolved
through the provider oEmbed, stored as
title/title_key, withtitle_attemptsandtitle_errorso deleted videos close out after the tries setting. Titles and previews share a batch (up to half for titles while both remain);previewCounts().titlePending,BackfillMember.pendingTitles, and the job totals carry the work, and the Bot tab member line shows+ N titles. Unit tests cover selection, resolution, and the give-up cap. Live: the user's queue (batch 40, 30 s, auto off) started once to verify — 133 titles resolved in two minutes, dead videos counting toward the cap. - C49 Searchable video titles. Embed rows now store Discord's
titleplus a normalizedtitle_key(lowercase, punctuation collapsed). The Advanced drawer has a title field with an Exact match (quotes) toggle: default is order-free multi-word prefix search ("Ma Ca Ju" finds Man who jumps over car), exact mode is one phrase; rows without a title drop out while the query is set. Store,/api/media,/api/media/stats, and MCP carrytitle/titleExact; the API result and card tooltip expose the title. Unit tests cover prefixes, mid-word misses, exact phrases, and punctuation folding; smoke checks the field and toggle. Note: titles only exist for rows scraped from now on — pre-existing rows need the backfill queued in TODO.md. - C48 Expanded providers and the Must include mirror. The list grew to 19 entries from the
user's picks: reddit, twitter, tiktok, instagram, streamable, facebook, gyazo, a merged
giphy,tenor(one checkbox, two substrings), 2ch.hk (stand-in for 4chan; the CDN hosts stay behind the default URL exclusion), funnyjunk, and internet archive. The drawer gained a Must include block mirroring Do not include: provider/source-kind checkboxes (includeProviders,includeSources), file-type chips, and the size/length bounds (moved here since they are inclusions, not exclusions). Store,/api/media,/api/media/stats, and MCP all carry the two include params. Live-verified:includeProviders=youtubereturns only YouTube rows,giphy,tenorincludes/excludes the same rows complementarily; smoke checks both groups against the API list (22 = 19 + 3 kinds). - C47 Provider list from the dataset. Aggregated the
providercolumn over all 11 scraper databases (distinct URLs): discord 13631 (folding cdn.discordapp.com and media.discordapp.net), youtube 4840, tenor 352, twitch 196, twitter 111, steam 83, medal 59, sketchfab 31, gfycat 30, imgur 23. Those ten now live inPROVIDER_FILTERS(media-scrape), are served in/api/media/statusasproviderFilters, and the Advanced drawer renders the source checkboxes from that list (4chan stays out; the default URL host exclusion covers it). Unit test for the list shape and a smoke check that the checkbox count matches the API list (13 = 10 + 3 kinds). - C46 Sort and exclusion filters. The Advanced drawer gained a sort select (newest, oldest,
user, size, length; global over the merged URL-dedupe stream) and a Do not include group
with nested collapsibles: sources (Twitch, YouTube, Discord, Tenor, Giphy, embeds, attachments,
stickers), file types, size (less/greater than with B/KB/MB/GB units), and length (seconds).
Store,
/api/media+/api/media/stats(shared parser), and MCPquery_mediaall carry the new params; size and length ranges drop rows with unknown values, and the ext matching now coalesces NULL content types so aNOT (...)cannot swallow rows. Also hid the media filter bar and drawer on the Bot tab. Unit tests for every exclusion, all five sorts (nulls last), and smoke checks for the hidden bar on the Bot tab. - C45 Streaming media grid. The grid rebuilt every card on each appended page, which made
filtered browsing flash, and the
Load moretrigger only worked once the grid could already scroll, so short filtered pages dead-stopped at ~11 cards. Now pages append incrementally (existing images are never rebuilt), a stream buffer capped at 1.5 viewport heights keeps the scroll alive while the next page loads, auto-fill cascades while the buffer is on screen, the buffer note readsLoading more…/Scroll for more/End of results, and an elastic nudge replaces the hard stop at the bottom. Smoke test: first load reaches 120 cards via auto-fill, scrolling appends 120 → 440 with no broken images. - 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), andetaTotalMs; the Bot tab shows only the total and putsstarts in X · runs for Yin the tooltip, same for the queue totals. Tests: lock ordering, stop semantics, ETA ordering. - C43 Bot-tab backfill control room and media-tab strip rules. The engine gained
previewBatch/rateLimitStatus/updateOptions; a newBackfillQueuegroups 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.MediaServicedrives it and exposesGET /api/media/backfillplus start/pause;/api/preferencesnow carriesmedia.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 redAPI timed out, increase timersbanner, 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-onlyN scrapes completednote. The oldVESKSLOP_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:pendingPreviewsnever 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). - 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; afterVESKSLOP_MEDIA_PREVIEW_MAX_ATTEMPTS(3) the row is markedunreachable after N attempt(s)and leaves the pending queue. Unit-tested. - C41 Restart keeps its logs. A button restart spawned the replacement with
stdio: ignore, so anything after the restart vanished. The replacement now appends todaemon.logbeside the token file (VESKSLOP_DAEMON_LOGoverrides); documented in docs/daemon.md. - C40 Stale scrape states. Two
general_chutscrapes showedrunningbecause the daemons that owned them were killed mid-job; the state file kept the last written value. On daemon start the engine now reconcilesrunning/queuedscrapes topaused, and History has a per-scrape Delete button (clean with a single ID) next to Resume. Unit-tested. - 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/webmreturn 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. - 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
defaultFiltershelper found by the Playwright smoke test, which passes on the new layout. - 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,cachedOnlyon/api/media,/api/media/stats, and MCPquery_media. Verified:origin=dmandcachedOnly=1return the expected rows. - 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, andGET /api/media/preview-failureslists 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 asanim_hash, served from/anim), and streaming only starts on explicit play.429s from previews/downloads raise a cooldown (VESKSLOP_MEDIA_RATELIMIT_PAUSE_MS, default 60 s, orRetry-After), leaving rows pending and pausing both walker and previews. - 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. - C34 Resume from cursor. A paused scrape can continue where it stopped instead of
re-walking the top:
POST /api/media/scrape/resume, MCPresume_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 pausedgernal_chutscrape resumed at cursor365256427247239181, fetched its final page, and entered the preview pass. - C33 Puppet history pagination. The core's
message.fetchread 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 channel591000745164406784: 21+ pages / 2,100+ messages and climbing, with hundreds of images/videos being indexed. - C32 Group-chat clarity and puppet refresh. Group DM scrapes already worked (live-verified
against the running daemon:
zıbb & fredand a 1:1 DM, backendpuppet, 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.fetchwithbefore: id+1, which returns exactly the target). - C31 Batched previews. The preview pass now runs in batches (default 25) with a cooldown
(default 60 s), configurable via
VESKSLOP_MEDIA_PREVIEW_BATCHandVESKSLOP_MEDIA_PREVIEW_BATCH_PAUSE_MS; batching counts rows, not successes. Live log:preview batch of 25 done, pausing 60000 ms. - 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, servingimage/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). - C29 Attachment video previews. Discord's image proxy renders a frame for video files
(
?width=320&height=320&format=webp), sopreviewUrlFornow resizes attachment mp4/webm in links mode too andpendingPreviewsincludes non-embed videos. Live: paused-scrape videos backfilled toimage/webpthumbs, pending queue drained to 0. - C28 Media grid usability. Infinite scroll (240 px threshold) with a Load more fallback,
previewErrorexposed 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. - C27 Non-media stream guard. Rows awaiting backfill fell back to first-frame
<video>with a provider page URL (YouTube embed, redirectors), and the stream route piped the HTML through (text/htmlerrors). Now only real video files (mp4/webm/m4v) are framed, the stream route returns415for non-media upstreams, the viewer's still-frame is gated the same way, and "Open in new tab" prefersplay_url. Live: YouTube item → 415 JSON; backfill reached 184/271 previews during the run. - C26 YouTube preview root cause. Dedupe picked the newest row per URL; a paused scrape
(
gernal_chut, stopped mid-walk) had no previews yet and hid an older scrape that did. Fixes: dedupe prefers the richer row (downloaded > preview > newer), paused walks still run the download/preview passes for collected pages, daemon start backfills previews for scrapes that paused early, and preview gaps are now 200-600 ms. Live: backfill cached 24 previews in the paused scrape within 45 s and a YouTube item then servedthumbUrl. - C25 Live scrape progress. The media tab polls
/api/media/scrapesevery 4 s while open, so the running row counts update; pages showX/Yp (~Z%)fromestimatedMessageswhen the search total is available (some channels deny the bot search with Missing Access, so it stays optional). Live: pages/messages incremented 2→10 during a walk ongernal_chut. - C24 Embeddable items open straight into their player: clicking a YouTube/Vimeo/Twitch/ Streamable card shows the iframe immediately, no extra play press. Non-embeddable videos keep the still + play icon.
- C23 Grid and viewer source fixes. The grid now prefers the cached still (or local file) for every kind, and only falls back to a first-frame video, served through the same-origin stream endpoint instead of hotlinked CDN/redirector URLs (fixes fxtwitter/YouTube previews that ORB blocked while the cached thumb sat unused). The viewer shows a muted first frame for videos without a still, and stream refreshes the signed URL only when the stored one fails (4xx). Live: stream returned 206 video/webm for a thumbnail-less video; YouTube rows keep their cached thumb.
- C22 Media grid polish: per-kind status row
(
(Images/Videos) | Downloaded I/V | Cached I/V | Loaded I/V), GIF tiles with a translucent GIF label that play the animated original on hover (cached previews are static ffmpeg frames), Tenor oEmbed preview resolution, and first-frame/inline playback for embeds that carry Discord'svideoUrl(Tenor/gifv now classify as video withplay_url).remotePreviewUrlexposes the animated source to the UI. Live: stats per kind correct; Tenor embed stored withplay_url. - C21 Playback of non-downloaded and expired media.
play_urlstores the embed's direct video (gifv/Tenor mp4); a new/api/media/<scrape>/<item>/streamroute serves the local file or re-fetches the source message through slopbot to refresh expired signed CDN URLs and streams it with Range support. The viewer and View full use it, so Discord attachment videos play without a prior download and old links no longer dead-end. Live: 2.5 MB webm streamed 200 with a refreshed URL; play_url populated for a media.discordapp.net gifv mp4 and provider redirectors. - C20 Cross-scrape dedupe and viewer polish.
queryMediaandstatsdeduplicate by URL (newest scrape wins), so overlapping scrapes no longer duplicate cards or counters (live: 17 items / 17 unique). Viewer: big play icon top-right; play embeds a provider player or a direct mp4/webm/m4v, otherwise opens the external warning; View full opens the embedded media/player in a new tab while the link button opens the original page; duration is stored (Discord attachment duration, ffprobe for downloads, 0 normalized to unknown); favicon added. - C19 External embed previews and MIME fixes. Embeds with no thumbnail in Discord's data are
resolved via provider oEmbed/fxtwitter and an
og:imagefallback; preview bytes keep their real extension (preview_ext), so JPEG thumbnails serve asimage/jpeginstead of mislabeled WebP (the browser "Content-Type image/webp is not supported" errors). The grid no longer uses embed page URLs as media, and first-frame<video>is limited to playable direct files (mp4/webm/m4v) with an on-error placeholder. Live: fxtwitter/vxReddit/TikTok previews cached; 7 jpg + 4 webp thumbs served with the right types. - C18 Video previews and the status row. Uploaded videos have no Discord thumbnails: the
grid renders a first-frame
<video>for them, and downloads generate a poster frame with ffmpeg intothumbs/(live: both video attachments in the test channel got posters). The status row is now(Images/Videos) | Downloaded A/B | Cached C/B (excl Downl) | Loaded L/Bwith B = total for the active filters. - C17 Media grid and viewer. Embed videos now render their cached thumbnail with a play
icon; a Metadata dropdown picks banner fields (User, Type, Date, Source, Resolution, Length);
nothing shows until Load media; the status row reads
downloaded A/B | cached C/D (excludes downloaded) | N loadedfrom/api/media/stats; clicking a tile opens a viewer with wide-below / vertical-right metadata, full link with an external-site warning, View full, and an embedded player for YouTube/Vimeo/Twitch/Streamable when possible. - C16 Message counting and date-bounded scrapes. Discord search (
total_results, one guild request, slopbot only) powersPOST /api/media/countand MCPcount_media_messages; the confirm dialog has From/To dates plus a Check Message Count button; the walk honorsafterId/beforeId;estimatedMessages,fromDate, andtoDateland in the scrape meta and History. Live: count 17 all / 17 after 2026-09-16 / 0 before, DM returns a reason, and a from-dated scrape stored 17 messages withestimatedMessages: 17. - C15 Scrape UX: custom download destination (absolute path per scrape; files there,
previews local, embed thumbnails copied to both, custom files not served), confirmation popup
before Links/Download, New scrape/History right-aligned with New scrape furthest right, modal
X and backdrop close, active-only progress strip, and a Bot error feed with a red unseen badge
that clears on open plus a Clear button. Live run
...-20260916095029: 6 files and 7 embed thumbnails in/tmp/opencode/media-dest/<id>/, 11 local previews, 0 local files,localUrlnull; error record and clear verified over the API. - C14 Links-mode embed thumbnails, item metadata, filters, and the scrape popup. Metadata:
poster (
author_id/author_name),posted_at,width/height,aspect_ratio, with anffprobefallback after downloads. Filters: poster, aspect preset (vertical/square/4:3/16:9/ 21:9/other), resolution preset (SD/HD/FHD/QHD/4K+/unknown, by shortest side), and a posted-date range. UI: scrape popup with Server and Group-chat trees (server -> channels, each with Links/Download), History popup (time, duration, pages/messages, media, bytes, files, previews), live progress pulse, full-window view. Live run...-20260916094120: 11 previews in links mode (embed thumbnails included), poster zzib, a 9:16 short stored as 0.563, and the 16:9 filter matched. - C13 Embed thumbnails in download mode: embed rows cache their stored thumbnail URL;
the full third-party media is never fetched. Live run
...-20260916091458on the test channel: 6 files downloaded, 11 previews cached (YouTube, Reddit, Tenor, TikTok, X). Also fixed the state transition: the job staysrunningthrough download and preview passes so clients do not seedoneearly. - C12 Media tab filters and full-window layout: kind chips, source filter
(attachment/sticker/embed), source-channel filter, and the view now fills the window below the
header and call strip. API/MCP gained
sourceandchannelIdonquery_media. Smoke-tested over/api/media?source=and?channelId=. - [~] C9 live verification. Slopbot path verified live on 2026-09-16: login Slopbot#5775, guild
177931418704281600. Test channel links scrape: 17 messages, 4 images + 13 videos, 0 errors; download scrape fetched the 784 KB attachment. Large channelgernal_chut: 11 pages, 1088 messages, 96 images + 1 GIF + 203 videos, 0 errors (no 429 at the current pacing). Puppet path verified live on a group DM (zıbb & fred) and a 1:1 DM: backendpuppet, done, 0 errors. The puppet field check is done too: the group DM returned attachments plus mapped embeds (YouTube, Instagram), and the large slopbot channel produced YouTube, Gfycat, Imgur, Steam, and Twitch embeds alongside attachments. Still owed: a stress walk on the second guild (559346566754729995, channel559346566754729997).
- C1 design docs, AGENTS/INDEX pointers,
-
Flextalk voice profile, end to end. The profile window is a movable panel that recenters on each open; it carries the character sliders with engine defaults and +/-2 snapping, five named presets, radio-kit effects (high-pass 40-400 Hz, echo 0-100, word mode with a gap in ms), and test lines (Attention, Laboratory, Critical, Health, Radiation, plus two custom lines in
localStorage) that post the current profile straight to/api/audio/speak. Fixed the test buttons reading a stale expression from saved state; they now read the slider like Speak does. -
Root cause of the profile never reaching the engine (2026-09-15): the wrapper wrote the Flextalk
.regwithecho, and dash'sechotreats\2in...\Watson\2.0\...as an escape, which corrupted the key sowine reg importwas a silent no-op (it still returns 0). The wrapper now usesprintf, validates that every profile value is numeric, and rejects a.regcontaining control characters. Documented in TTS-DESIGN.md. -
The Veskslop agent lives in
agent/(Go 1.25+, Eino v0.9.19, remote llama.cpp over the OpenAI-compatible API, daemon tools over MCP Streamable HTTP). It is disabled by default (VESKSLOP_AGENT_ENABLED=1), takes tasks onPOST /task, and logs every tool call. Smoke-tested on 2026-09-14: 28 tools loaded, one ReAct round, final message returned. The daemon does not push tasks to it yet. Plan and env vars: docs/agent.md. -
Remote agents use the daemon's MCP endpoint directly (query state, send notifications). A notification tool set and a documented LAN/Tailscale bind are planned.
-
Mobile web UI first draft: four tabs (Users, Chat, Call, Activity), one pane at a time, 44px targets, tab badge, call bar pinned, Activity becomes the full console. Verified headless at 390x844. Next: transcript and TTS box in the Call tab, per the 1c design.
-
Fixed a pre-existing web UI bug:
loadDmscalled the nonexistentrenderDirect, so the DM rail only rendered from other paths. It now callsrenderDmRail(). -
Fixed the speaking marks: the core turned Discord's numeric
speaking: 0intotrue, so the equalizer bars never stopped. The bars now fade in on start and decay out on stop (500 ms), with a 2.5 s idle timeout for missed stops; a mute or channel change ends the state at once. Muted members show no bars. -
Browser audio: Client audio now runs the browser unlock in the click (microphone permission, AudioContext created and resumed, output sink applied) before it starts listening. Verified headless: 52 PCM frames played through a running context. The button label no longer wipes the icon markup.
-
Capture routing: nothing moved the client's playback streams into
veskslop_capture, so the browser monitor streamed digital silence. The audio manager now adopts matching streams (VESKSLOP_AUDIO_CLIENT_MATCH, defaultvesktop,electron,chromium,discord) on device setup, monitor start, and every 5 s while listening or capturing. -
LAN audio verified end to end on 2026-09-14: a tone stream on
veskslop_ttswas moved toveskslop_capture(daemon log:moved stream 81 to veskpop_capture) and the browser, connected tohttps://192.168.0.178:18801/, played 256+ PCM frames with a peak of 4096 (a stream already on the capture sink peaked at 29274). -
Speaking bars are always in the layout now:
scaleY(0)and transparent when idle, they scale up with the fade-in at start, run the equalizer animation while speaking, and stay on until an explicit stop, a mute, or a channel change. Live data showed repeatedspeaking: truepulses and no stop events inside an 8 s window, so the idle decay is removed: bars stick rather than disappear. The fade runs the equalizer inside a shrinking container, so the amplitude drops smoothly instead of the block collapsing at full height. The core treats a missingspeakingflag as a start pulse and acceptsuser_id. Core-side changes need a rebuild plus a client restart. -
Added
poc/scripts/audio-doctor.sh(pnpm audio:doctor): default sink, sink inputs, audio status with adoption counters, a 5 svolumedetectonveskslop_capture.monitor, and a live event peek. Run it in a real call to trace a silent browser monitor. -
Live doctor output (2026-09-14): the
vesktop/electronstreams sit onveskslop_capture(sink 40) already,adopted: 0, andVOICE_SPEAKINGevents arrive. Client audio routing is therefore in place. Bot TTS works. Capture was off (capturing: false), so the transcript was empty; Vosk transcribes correctly when invoked with the model env. -
Live call audio works now (the user confirmed TTS is audible and the silent call was partly a per-user volume set to zero for some members, plus another issue). The
volumeread returnsnullbecause this client build exposes noUserVolumeStore; the setter reportsNOT_SUPPORTED. Volume is not a usable control here. -
External TTS engine support (external project, first target is a command wrapper): the contract lives in
TTS-DESIGN.md. Veskslop now has nullabletts.speed(0.5-2.0) andtts.pitch(-12..+12) preferences, per-line overrides on/api/audio/speakand MCPspeak,{speed}/{pitch}placeholders,VESKSLOP_TTS_SPEED/_PITCHexports, and speed/pitch sliders in the TTS panel. A custom command advertises its voices throughVESKSLOP_TTS_VOICES; the espeak list is hidden while a custom command is set. Verified with a wrapper that logged argv and env: preferences and overrides both arrive. Reading the engine'sGET /voicesfor the HTTP provider remains future work. -
The MicroslopSam engine project is now part of the tree in
microslopsam/(copied from the separate project; itsbuild/output and voice data stay ignored). The wrapper was renamedsetup/vekslop-tts.sh->setup/veskslop-tts.shand every reference and configuration export was fixed. The engine also corrected the Flextalk pitch finding (REG_DWORD bug), so Keith Bell and Kerry Watson now support pitch and carry nono-pitchflag. -
Implemented the Flextalk Expression profile parameter end to end:
VESKSLOP_TTS_VOICESmarks the two voices asexpression=59/expression=100; the wrapper writes the registry value before synthesis; Veskslop hastts.expression(0-100 nullable), the{expression}placeholder,VESKSLOP_TTS_EXPRESSION, an HTTP/MCP field, and an Expression slider that appears only for those voices (default shown until moved, null leaves the registry untouched). Verified: 0/59/100 produce distinct audio, the registry reaches 0x14 for 20, and the UI shows 25/100/default states. -
Fixed the null-value coercion in the preference normalizers (
Number(null) === 0turned a cleared speed/expression into 0.5/0); unset now stays null. -
Renderer-down handling polished: the bridge logs one warning per outage instead of a stack per polled command, and the
navigator.languagesrequest catches the new timeout rejection instead of raising an unhandled promise warning. (TheexitCode: 9renderer kills in the live log were client restarts; the daemon's retry handled the startup race as designed.) -
The TTS panel has speed presets (0.5x, 0.75x, 1x, 1.25x, 1.5x) under the fine-adjust slider, and the pitch control is hidden entirely for voices that report
supportsPitch: false. (Flextalk voices no longer report that; see the pitch correction above.) Verified: preset clicks update the slider/label and save. -
TTS playback has a lead-in of silence before each line (
VESKSLOP_TTS_LEAD_MS, default 150 ms, max 2000, added withadelayinffplay; status reportsttsLeadMs) so Discord's voice pipeline is settled before the first syllable. Verified on the sink monitor: first sound at 0.02 s with 0 ms and 0.62 s with 600 ms. -
Speech settings have a Start engine button that warms the selected engine (
POST /api/audio/tts/warm, MCPstart_tts_engine): it runs one throwaway synthesis so the command engine's Xvfb/Wine/host are up. Verified with MicroslopSam: cold 1732 ms, warm 505 ms, and the UI shows "MicroslopSam ready in 835 ms". The warm text is a full sentence because their two-passloudnormfails withinfon very short input. -
Startup and crash robustness:
sendRendererCommandnow rejects immediately when the client renderer is destroyed or crashed, and times out after 15 s, so a dead frame no longer logs Electron's "Render frame was disposed" stack once per poll. Device auto-connect retries up to 10 times every 2.5 s, because the bridge socket is ready before the renderer's modules load (the live error wasReflect.get called on non-objectfrom a lazy lookup at startup). Verified with the mock: attempt 1 failed, attempt 2 set the devices. -
TTS engines are selectable: the Speech settings and the TTS panel list espeak-ng, the command engine (
VESKSLOP_TTS_NAME, default "custom engine"), and the HTTP API; unavailable entries are disabled and Auto resolves to the command engine first. Selecting an engine switches the voice list (/api/audio/tts/voices?engine=) and hides the language row for single-language engines. The TTS panel now has a voice select next to the speed/pitch sliders. Fixed a slider bug where a null speed preference (Number(null) === 0) clamped the slider to 0.5 and then persisted it. -
TTS controls: the TTS panel has a working duck on voice toggle (stored in preferences, reported as
duckEnabled). Ducking was verified live with the mock: theveskslop_ttssink drops to 30% while TTS plays and someone speaks, and returns to 100% after. The Transcript tab in the console has Start and Stop buttons; the status line showscapturing/capture off. The espeak-ng voice list now includes them1–m7,f1–f5,croak, andwhispervariants (<voice>+<variant>), so each language has about 15 voices instead of one. -
Speaking state is now authoritative: the daemon polls the core's
voice.speakingaction (ChannelRTCStore.getSpeakingParticipants) every second and synthesizes the missing stop/start events; dispatcherVOICE_SPEAKINGhints feed the same set so stale users clear. This fixes the bars sticking for everyone and the transcript attribution, which credited the first member because speaker intervals never closed. Verified with the mock: aspeaking: trueevent cleared within one poll. -
The client streams sit uncorked, unmuted, and at 100% on
veskslop_capture, yet the monitor is silent. GatewayVOICE_SPEAKINGevents do not prove inbound RTP, and the TTS path only proves outbound, so the remaining suspect is the client's inbound voice state (stuck deafen or a broken voice connection), not the sink routing./healthnow returnsselfIdso the bot's own member flags can be picked out of the voice member list; the doctor prints it. -
Client output is still silent at the sink (
-91 dB) although the streams are uncorked, unmuted, and at 100%, and a headless Chromium tone into the same sink measured-7.4 dB. Added per-user volume reading and writing to find Discord-side silencing:user.volume/user.setVolumein the core,/api/voice/volume(GET and POST), MCPget_user_volume/set_user_volume, andvolumein every voice member. A volume of 0 (or an output volume of 0) silences a user even when Pulse shows a healthy stream.voice.joinalso clears persisted self-deafen now. The doctor prints output and per-user volumes. -
Voice audio is implemented: daemon-managed virtual devices (
veskslop_tts,veskslop_mic,veskslop_capture), TTS providers (local command + HTTP API), queued playback throughffplay, ducking onVOICE_SPEAKING, and WAV capture. The daemon startspulseaudiowhenpactlcannot reach a server and reports install hints. Verified live on 2026-09-13 after installingpulseaudio/ffmpeg: engine ok, devices created, PCM streamed. -
The Web UI serves self-signed HTTPS (
openssl, stored under the state dir) so browser media APIs have a secure context.WEBUI_TLS=0disables it. -
Capture and transcript are implemented: ffmpeg reads the client output, a VAD cuts it into utterances, each becomes a WAV plus a transcript entry with the dominant speaker, and a pluggable STT hook (local command or HTTP API) fills the text. Live events feed the console Transcript tab;
get_transcriptand/api/transcriptread the store.VESKSLOP_CAPTURE_AUTO=1records calls automatically. Verified end to end with real PulseAudio on 2026-09-13. -
STT uses Vosk (CPU, light):
poc/scripts/stt-vosk.pyplus a small model inpoc/models/(auto-detected). Verified with a TTS→capture→Vosk round trip. Other providers remain available viaVESKSLOP_STT_CMD/_API_URL. -
TTS defaults to
espeak-ngwhen installed; engine and voice live in Settings → Client → Speech (daemon preferences, so all browsers share them). -
Preferences moved to
<state dir>/preferences.jsonand no longer depend on the core, so guild visibility saves even while the client is down. API responses sendCache-Control: no-store. -
Browser audio has an Enable browser audio button that requests the microphone permission, which browsers require before listing output devices.
-
Browser microphone puppeting (talking through the bot) is next; it needs a mix sink, a
pacatinput, and echo handling. -
The web UI follows the Nocturne design handoff (
poc/WebUIMockup.zip). The 7a shell is implemented and rendered headless for review: header, call strip, three-column body, composer, and the agent console. -
Takeover is a client-side read-only gate. Off, the composer is locked. Manual sends are logged.
-
Core process control is implemented and verified live: the daemon starts the client under auto-Xvfb, the bridge connects, and stop exits cleanly.
-
Web QR login is implemented. The session reached
pendingagainst the real gateway; a full phone approval is pending.
Done
-
Validated core bridge: protocol, actions, validation, structured errors, hard outbound rate limit.
-
Auth persistence: pinned
DATA_DIR, token file, preload seeding,VESKSLOP_HEADLESS. -
Daemon on the core-bridge transport. CDP stays as a development fallback.
-
Trusted-only triggers: DMs and direct mentions. Trust tiers: user, trusted, admin.
-
Context: 15 messages or 4000 characters, time stamps, file names only, incremental fetch, tmp cache with a size limit, fetch cooldown.
-
Continuation window: a rushed follow-up joins the request context.
-
Response policy: origin channel or DM. Sensitive commands default to DM.
-
Persistent action log: triggers, commands, replies, errors, rotation, and secret redaction.
-
pollis paused (POLL_ENABLED = false). The parser and its tests stay. -
Human send pacing: first send of a burst waits 30-200 ms, later sends 500-2800 ms, and the burst resets after a random 5-20 s of quiet. Reads take 100-300 ms and do not reset the burst.
-
user.search: cache and guild-member lookup.voice.getsearches all guilds without aguildId. -
voice.overview: occupied voice channels and members for a guild. -
VOICE_STATE_UPDATEevents: normalized and pushed. Mock-verified. -
Voice actions:
voice.join(muted),voice.leave, andvoice.movein the core, with daemon routes (/api/voice/*), MCP tools, and Web UI wiring. Voice actions are paced 250-750 ms and written to the action log. The internal lookups were probed on the live client. -
Web UI voice rail: every voice channel is clickable. Clicking joins (muted); clicking the active channel leaves. Verified with the stateful mock.
-
Web UI startup: a Start server button appears left of Takeover while the bridge is down and hides when it connects. State reloads retry while the client boots, so guilds appear without a manual refresh. Core requests now time out after
VESKSLOP_REQUEST_TIMEOUT_MSinstead of hanging forever on a missing reply. -
Settings: "Visible guilds" stays top level; Audio sits under a collapsible "Client" group; Core process, Account login, and Action log sit under a collapsible "Server" group.
-
Web UI timestamps use the browser timezone by default; Settings → Client → Display can override it with any IANA zone. The choice is per browser (
localStorage). -
Web UI preferences: visible guilds persist server-side in the daemon state file (
/api/preferences), so every browser shares them. The Settings panel has an explicit Save button; selections filter the UI live and persist only on Save. -
DM list: recipients resolve from IDs through the user store, so names and avatars load.
-
Guild menu: fixed the light default button background.
-
Web UI: Nocturne design tokens in
poc/public/nocturne.css, the 7a debug shell, live guild and channel rail, messages, DMs, voice membership, agent console with a live event tab, and a Settings slide-over for core process, login, visible guilds, and the action log. Roadmap surfaces show as placeholders. -
Web UI data: opening a chat fetches the latest 50 messages into the client cache; attachments hotlink Discord CDN URLs and are never stored; avatars and guild icons load from the CDN with a monogram fallback; the page reloads itself when the bridge connects.
-
Web UI drafts: composer and TTS text persist per channel in
localStorageand survive re-renders, reconnects, and page reloads. -
Visible guilds: the Settings panel filters the guild switcher and picker.
-
Takeover: client-side read-only switch with
localStoragepersistence; manual sends are written to the action log withtype: "manual". -
dm.list: cached DM and group DM channels, exposed over MCP aslist_dms. -
Core process control: the daemon starts, stops, and restarts the client, reconnects the bridge after a restart, auto-starts Xvfb when
DISPLAYis missing, and stops what it started on exit. Override withVESKSLOP_CORE_CMD/ARGS; disable Xvfb withVESKSLOP_XVFB=0. -
Web QR login: the Settings panel runs the remote-auth session and renders the QR. The token file is written on approval and a running managed core restarts with the new token.
-
Unit tests cover the pacing policy.
pnpm lintandpnpm testTypespass at the root. -
Web UI navigation: Guild/DM scope tabs, a DM rail with unread badges (purple/orange, grey while Takeover is off), last-message times, favourite stars, and a People list of users seen in voice. Favourited users get presence and call tracking, grouped by call in In voice. Speaking shows level bars and bold names (2c marks for mute/deaf/video/share). The body dims while the bridge is down; the Start button becomes Refresh when the daemon is unreachable.
-
TTS settings are split into language and voice, grouped by base language with the requested languages pinned on top.
-
Text notifications are parsed: edits update the context cache, deletes drop from it, trusted reactions are logged, and replies are kept in the request context. A reply to a puppet message counts as a trigger without a mention. The web UI marks edited messages and replies.
-
Voice auto-leave is implemented: when the owner (zzib) leaves the bot's call, the bot leaves after
voiceLeaveDelaySeconds(default 30). A rejoin cancels it. Verified with the mock, including cancellation.stay @useris deferred. -
Audio verified end to end on 2026-09-14: TTS played into the virtual mic (recording
veskslop_micpeaked at -4.1 dB) and the browser monitor streamed PCM over the TLS WebSocket. -
Next: per-user limits and the collector account. The agent attachment (remote llama.cpp, MCP transport, event delivery) waits for a design discussion; the draft prompt has open issues to settle first.
Next (in order)
- Live verification:
voice.join/voice.leaveend to end,user.search,voice.overview,VOICE_STATE_UPDATE, and a full web QR login approval on the real client. - Agent: daemon task push for free-form triggers (commands stay local), tool allowlist, per-conversation serialization, and tests against a fake model.
- Collector account: read-only modules for voice membership fallback.
- Per-user rate limit and command cooldowns.
- Mobile web UI: transcript and TTS in the Call tab; verify on a phone.
Open discussions (need the user)
- Web UI auth. Accepted: no auth, LAN or Tailscale only. Revisit before any wider exposure.
- MCP event delivery. Decided for the local harness: the daemon pushes tasks to the agent's
POST /task. The transport details (retry, dedupe IDs, queue) are still open. Remote agents poll or call tools directly; they do not receive pushes. - Untrusted logging. Untrusted interactions are ignored at this time. Confirm whether to log untrusted DMs and mentions again.
- Privacy pass. Only token-shaped and
mfa.strings are redacted, and only in the persistent action log. A fuller policy (retention, cache clearing, message redaction) is deferred. - Live checks.
reaction.add,user.search,voice.overview, and the voice state event normalization are mock-verified only. The QR login session reachedpendingandcancelagainst the real gateway; the approval path needs the phone. Confirm on the first real-client run.
Blocked / needs input
- Slopbot: the bot is invited to the test guild and the intent is enabled. The token lives in
poc/.env(gitignored, mode 600) and will be replaced when the project goes to prod. The daemon must be restarted to pick it up. - The original repo
zib/Veskslophas a broken server-side git setup. Fetches and clones fail withbad pack headerbecause a script (/home/git/.sys_health_s3) runs on the Veskslop read path. The admin must fix it. Usezib/Veskslop-t(remoteveskslop-t) until then. That remote has no stored credential; use the one-off token URL inGITEA.md. Rechecked 2026-09-16: the original still fails withbad pack header. mainon the original server is68fc8bb(zib, "Upload files to docs/images"). It diverged from the localmain(24447f5). Reconcile after the server is fixed.
Where to look
- Backlog:
TODO.md - Design documents:
docs/INDEX.md - Design handoff:
poc/WebUIMockup.zip(screen 7a is implemented) - Agent rules:
AGENTS.md - Daemon code:
poc/src/ - Web UI code:
poc/src/webui.ts,poc/public/ - Core bridge code:
src/shared/puppetProtocol.ts,src/renderer/puppet/,src/main/puppetBridge.ts