Maintainer principle: put as much effort into the static bitvid client as
possible before branching into any separate application.
- bitvid-bridge-plan: prominent "PRIORITY PRINCIPLE — static bitvid first" banner
up top; the Bridge (a second app) is deliberately DEFERRED until the static
client's live features (Go-Live Tier-1/2 MVP) ship and are maximally developed;
build it only when live proves demand for its sovereignty/power features.
- live-publish-plan: new "Build order & the static-first principle" section with
the recommended sequence — 1) Shorts (#16b), 2) Live ingest (#16 watch
foundation), 3) Go-Live MVP (#16c P0-1, Tiers 1&2, client-side only, no
Bridge/VPS), then 4) the Bridge later only if warranted.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Answering "can we avoid the VPS / use a free service like StreamYard?": yes.
live-publish-plan:
- DECISION 1 broadened into a THREE-TIER provider spectrum (bitvid hosts none):
(1) platform embed (StreamYard/OBS -> YouTube/Twitch, free, iframe),
(2) serverless RTMP->HLS API (Livepeer/Mux/Cloudflare, no server, NATIVE HLS) --
the recommended no-VPS default; Livepeer most aligned,
(3) self-hosted node (Bridge headless on VPS) -- sovereignty + WebTorrent VOD.
- New "Provider options" table/section; StreamYard clarified as a studio (not a
CDN). Same NIP-53 event + player across all tiers; only a per-tier playback
adapter differs. Phase 1 reframed as provider-agnostic (Tiers 1 & 2, no infra).
bitvid-bridge-plan:
- Repositioned as the Tier-3 advanced/sovereignty option (serverless is the no-VPS
default), not the default on-ramp.
- Added the HYBRID that dissolves the NAT/VPS problem: a local Bridge records
locally + forwards (outbound) to a Tier-2 serverless provider that serves public
HLS -> home box needs no public reachability, yet keeps local recording +
full-WebTorrent VOD. Made serverless a first-class restream preset (Phase 3);
updated the reachability risk + NAT notes to point at the hybrid.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Maintainer blanket rule: bitvid.network runs NO servers / media infra, ever. All
media receiving/routing is self-hosted by the creator (the Bridge run headless) or
handled by an external provider.
live-publish-plan:
- Add the blanket rule as a non-negotiable principle in the Core Decision.
- Lock all 6 decisions: D1 self-host OR external only (managed bitvid node
REMOVED); D2 signing A-then-NIP46, managed account (D) removed; D3 pass-through;
D4 HLS-first; D5 Bridge IS the self-host path; D6 keep specific flags.
- Sweep every "managed / public BitVid node" reference: the Media Node is always
self-hosted (= Bridge headless per DECISION 7) or external; Phase 3 reframed as
a reference node the CREATOR runs; risks reframed (burden is the self-hoster's,
not bitvid's). Status: decisions locked.
bitvid-bridge-plan:
- Remove "public Media Node" implications; the Bridge is the self-hosting vehicle.
- Add the reachability/NAT reality: a home-only Bridge can't serve public HLS →
headless-on-VPS is the path (why DECISION 7 matters).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
live-publish-plan:
- Expand the config-flags section into a full enumerated "off = no trace" list:
every Go-Live surface (Studio/entry points, My Videos Live sub-tab, live-event
builders, Bridge/session UI, restream UI, archive->VOD flow, routes) is absent
when FEATURE_LIVE_PUBLISH is off. Bridge/restream have their own sub-flags.
- Clarify watch/publish independence (an instance can allow watching without
broadcasting).
- New gap closed: WHO may go live — publishing should respect the whitelist/WoT
(operators can restrict broadcasting to whitelisted creators; don't be an open
ingest relay).
bitvid-bridge-plan:
- Invariant 6: the Bridge respects the connected instance's config (embedded
webview inherits FEATURE_LIVE_PUBLISH; go-live degrades gracefully when off).
- Invariant 7: bind local ingest to loopback (127.0.0.1) only; auth required for
any wider bind. Matching risks added.
- Architecture: Studio webview loads the instance's deployed web app (remote,
cached) so it auto-inherits instance flags and never drifts; instance URL
configurable.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Expands live-publish-plan Phase 7 into docs/bitvid-bridge-plan.md: a super-simple
Windows/macOS/Linux app that makes going live turnkey.
Core design: native media core (MediaMTX + FFmpeg + rclone; never sees the Nostr
key) + an embedded bitvid "Studio" webview that signs — talking over in-app IPC,
NOT browser->localhost (dodges mixed-content/PNA and keeps signing in the web
layer, honoring "BitVid signs; Bridge routes").
Surfaced 10 decisions: framework (Wails/Go vs Tauri), control model (embedded
webview vs website-drives-localhost), capture vs receive, webview signer support
(NIP-46 + encrypted nsec), ingest protocols (RTMP MVP), upload (bundled rclone),
one-codebase-two-modes (Bridge desktop == self-hosted Media Node headless),
pass-through-only transcode, code-signing/notarization ownership (the real cost of
"easy install"), license/telemetry.
Phases 0-7 (shell+core -> receive MVP -> record/VOD -> restream -> turnkey/packaging
-> built-in capture -> SRT/WHIP/WebRTC -> headless node + BitVid Box), invariants,
packaging/signing, risks, sources. Linked from the publish plan.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Answering "can live use WebTorrent?": classic WebTorrent can't carry live (its
infohash needs the complete file), but P2P *live* is real via HLS + peer segment
sharing.
- live-ingest-plan: new "P2P-assisted playback" section — P2P Media Loader
(hls.js-compatible, historically WebTorrent-tracker-based) shares HLS segments
over WebRTC to offload origin bandwidth. Gated on real concurrency (DECISION 1's
whitelist scope makes live sparse), so v1 = plain hls.js, P2P later. VOD keeps
full WebTorrent.
- live-publish-plan: new "Why BitVid Live — advantages" section (P2P offload,
full-WebTorrent VODs, Nostr-native/no-lock-in, built-in zaps, BYO storage/relays/
self-host, one-canonical-stream+mirrors, archive = first-class video, self-
sovereign identity). Playback section cross-references the P2P offload + VOD
WebTorrent.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Third live/short plan, sequenced after #16 (ingest) + #16b (shorts). Turns the
maintainer's BitVid Live design into a repo-accurate, convention-aligned plan:
- Core decision: Nostr-native control layer, NOT a hosting backend. Static client
stays static (signs, no keys, no server); the one thing a browser can't do
(receive/route live media) is isolated in an OPTIONAL server-side Media
Node/Bridge (MediaMTX). External-provider-only mode (Phase 1) needs zero infra.
- Cross-linked to docs/live-ingest-plan.md (#16): the watch side (Live tab,
player, chat, zaps, hls.js) is a dependency and reused; this doc adds the
builders + Studio + Media Node/Bridge. Shared Phase 0 schema in nostrEventSchemas.
- Verified against real code: NIP-98 HTTP-Auth (kind 27235) builder exists for
bridge auth; s-tag + js/utils/storagePointer.js -> .info.json is exactly how
archive->VOD maps (ended stream becomes an ordinary kind-30078 video + 34235
mirror); canonical video kind 30078.
- Decisions surfaced: hosting model, signing-during-stream (tab-open vs NIP-46),
pass-through vs transcode, WebRTC scope, local Bridge scope, flag reconciliation
(FEATURE_LIVE_INGEST/PUBLISH/BRIDGE/RESTREAM).
- 9 phases (0 flags+schema → 1 external-provider no-infra → 2 mock bridge → 3
Media Node MVP → 4 recording/VOD → 5 restream → 6 OBS companion → 7 local
Bridge → 8 WebRTC), 5 engineering-rule invariants, risks, sources.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- D1 whitelisted hosts only (empty-when-nobody-live accepted).
- D2 read + post + zaps as the end state, phased: Phase 3 read chat + host zaps,
Phase 3b post chat (own sub-flag + moderation stance).
- D3 hls.js, lazy-loaded + vendored behind the flag (native HLS is Safari-only).
- D4 one Live tab: live first, then recent ended (VOD) — also mitigates the
sparse-live empty-tab concern.
- D5 user read relays + a configurable set of live-discovery relays (widens
where, not who).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- D8: native short = kind 30078 + isShort marker (mirrors to 34236); marker is
fine since a single isShort boolean reliably distinguishes shorts everywhere.
- D5: Option C phased — Phase 1 grid + portrait card + main-feed exclusion,
Phase 2 immersive feed later.
Shorts plan decisions now locked except D6 (card: VideoCard variant vs dedicated
ShortCard) which is a build-time implementation choice.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Shorts plan decisions from maintainer walkthrough:
- D1 whitelist scope, D2 inherit moderation (+ NSFW no-autoplay safeguard),
D3 Shorts tab ONLY (exclude from main feeds), D4 kind is the definer.
- D7 (agreed): upload-modal content-type selector + explicit isShort marker on
the kind-30078 schema (native shorts need it; kind-22 only covers ingested).
- D8 (leaning marker): native short = 30078 + isShort marker mirrored to 34236,
OR a distinct kind that also mirrors to 34236 — both yield the same external
result (WebTorrent-rich native + standard 34236 mirror so other NIP-71 apps
see bitvid shorts). Marker recommended; maintainer to confirm.
- Publish/management UI is flag-gated (off = no trace): no upload selector, no
My Videos Shorts sub-tab when FEATURE_SHORTS off.
- My Videos gets Videos/Shorts/Live sub-tabs (reusing the admin sub-tab pattern);
Live sub-tab tied to publish (#16c), not ingest. Noted in both plans.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fold July-2026 live-streaming watch UX into docs/live-ingest-plan.md:
- Desktop cinematic layout: 16:9 player + collapsible chat side rail, host/
LIVE badge/viewer count below, theater + fullscreen, keyboard controls.
- Mobile: player-top / chat-below, resizable chat overlay in fullscreen.
- Live-specific: LIVE badge + viewer count, auto-scroll chat w/ pause-on-scroll,
live vs ended seeking (VOD scrubber in Phase 4).
- Latency reality baked in: HLS ~6-30s behind → chat runs ahead of video; don't
promise low-latency.
- bitvid angle: live zaps to the host (zap.stream-style) fit the existing zap
system and can ship even before/with read-only chat.
- Phase 2 now includes the cinematic layout; sources cited.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fold July-2026 short-form UX research into docs/shorts-plan.md:
- Immersive one-at-a-time 9:16 feed (grid = entry point), muted autoplay + loop,
preload next 2-3 (no spinner between shorts), overlaid action rail
(Zap/Comment/Share/follow — zaps as the bitvid differentiator), captions when
present, prefers-reduced-motion.
- Mobile: full-screen + swipe up/down.
- Desktop: centered letterboxed player (never stretch 9:16) + NATIVE keyboard nav
(up/down prev/next, Space, M, Esc) + side action rail — YouTube Shorts' desktop
keyboard-nav gap is an opportunity to do it right.
- Phase 2 reframed as the responsive immersive feed; sources cited.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Shorts (docs/shorts-plan.md):
- Add DECISION 6 + architecture/phase items for a NEW portrait 9:16 card design.
The existing VideoCard.js is hardcoded 16:9 (ratio-16-9), so shorts need either
a "short" variant of VideoCard or a dedicated ShortCard — a first-class Phase 1
deliverable, not an afterthought.
Live ingest (docs/live-ingest-plan.md):
- Reframe past streams (VOD) and chat from "deferred/maybe" to committed phases:
Phase 3 read-only chat (+ optional 3b post), Phase 4 ended/recording VOD
playback through the same HLS path. So the plan's phases fully account for
live AND past zap.stream streams AND chat.
- Add a distinct LiveCard design (LIVE badge, viewer count, ended state).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two flag-gated, watch-only content types, each spec'd following the repo's
docs/*-plan.md convention with open decisions embedded as DECISION callouts.
- docs/shorts-plan.md (#16b): NIP-71 kind 22. Ingest already exists
(nip71IngestAdapter handles kind 22), so this is an isShort marker + a Shorts
sidebar tab + vertical UI + a shorts feed source. Behind FEATURE_SHORTS.
- docs/live-ingest-plan.md (#16): NIP-53 kind 30311 (+1311 chat), HLS playback.
Leads with a Phase 0 research spike (confirm tag shapes from zap.stream/
shosho.live, de-risk hls.js + CORS). Behind FEATURE_LIVE_INGEST; publishing
(#16c) is explicitly out of scope with its own future flag.
Both docs specify the "off = no trace" wiring (no tab, no feed registration, no
subscriptions, no route, no lazy assets) per the locked shared requirement.
TODO #16/#16b now link their plans.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Makes the card liveness-visibility behavior switchable so it can be A/B
feel-tested, and probes ahead of the scroll for a snappier result.
- CARD_LIVENESS_POLICY (config/instance-config.js): "show-pending" (default,
unchanged) | "hide-foreign" (foreign/ingested cards hidden until a source
verifies; native cards stay show-pending) | "hide-all". Owner's own cards are
always visible. Flip live for testing without a rebuild:
window.__BITVID_CARD_LIVENESS_POLICY__ = "hide-foreign" then refresh/scroll.
Implemented via a data-foreign provenance flag on the card +
getCardLivenessPolicy() in cardSourceVisibility.js.
- LIVENESS_PROBE_PREFETCH_MARGIN (default 600px): both liveness probes were
already viewport-gated (IntersectionObserver, on-screen-first, WebTorrent
priority-queued + concurrency-capped); this expands their rootMargin so cards
just below the fold verify before the user reaches them.
- Tests: three policy scenarios in tests/video-card-source-visibility.test.mjs;
also un-broke 3 pre-existing VideoCard tests by adding the missing
IntersectionObserver JSDOM stub.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Finishes the remaining #3 zap audit items.
- zapSplit: when the platform fee would pay the SAME Lightning address as the
creator (e.g. the creator is the platform operator), collapse to a single
full-amount payment instead of sending two invoices to one wallet — avoids an
extra routing fee and a confusing duplicate receipt for the same total.
- Audited split correctness: floor(amount*fee/100) with the creator taking the
remainder loses no sats; a sub-1-sat fee rounds to 0 and the whole zap goes to
the creator (no zero-amount platform send); fee is clamped 0–100 and a junk
override falls back to the default. The earlier "fee landed in my own wallet"
report is expected behavior (sender is the platform operator, or a self-zap),
not a bug.
- Cheat-resistant tests: testCreatorEqualsPlatformCollapsesToOnePayment and
testSplitRoundingPreservesEverySat in tests/zap-split.test.mjs.
- Receipt validation (9735) and NWC budget/retry UX confirmed already solid;
audit plan + TODO #3 closed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Record this session's fixes (delete/zombie, NIP-71 mirror teardown,
watch-history bucketing, the zap chain, the test-runner quarantine work)
and the test spec-corrections.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Defer the Capacitor native-app plan and the NIP-46 roadmap enhancements
(current NIP-46 login works well) while the pre-launch backlog clears.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- docs/zap-audit-plan.md: architecture map, findings (LNURL CORS send-error,
comment box confirmed working, popover mis-position), and prioritized next
steps (CORS/LNURL proxy decision first, then popover, fee-split correctness,
receipt validation, NWC UX). Notes the injectable-fetcher/deps test seams.
- TODO #3 points to the doc; #9 adds two unverified learnings: confirm the
community-blacklist batching actually dropped the kind-30000 storm (still saw
=32 in a later load), and trace the `from_hex` malformed-hex source via the
new sanitizer dev-log.
Planning/docs only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Audited the card liveness/health system. Policy (cardSourceVisibility) is
already correct — hides only when neither CDN nor WebTorrent is healthy,
un-hides when WebTorrent flips green. Documents the real gaps (slow probes;
opaque HEAD no-cors inaccuracy for cross-origin NIP-71 CDNs; single-source
adapter dropping extra imeta) and a 3-phase plan: multi-source capture,
video-element playability probe across sources, and speed tuning. Adds
docs/video-liveness-plan.md + expands pre-launch TODO #5. Planning only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Imports not produced by bitvid's storage system (reference imports / external
URLs) must be explicitly flagged externally-managed so storage tooling never
treats them as bitvid-owned: external badge (never "missing from bucket"),
excluded from orphan reconciliation, delete "file left behind" warning
suppressed, liveness unverifiable. Adds import modes (full re-host vs
reference) + Phase 2b to the plan and TODO 17c. Notes the existing
isUrlUnderBase heuristic is the baseline but a durable flag is needed.
Planning only; no code changes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds docs/nip71-onboarding-plan.md and pre-launch TODO 17c for the agreed
two-way NIP-71 interop:
- Phase 1 (first): cross-ecosystem dedup — prefer the bitvid (30078) version,
show exactly one in every grid; identity = author + import-link/hash/infohash.
- Phase 2: creator-initiated self-import (model A) — re-host to the creator's
storage (CDN) + WebTorrent, publish a signed 30078 with an imported-from
provenance tag. No forging others' notes; no instance auto-rehost.
Planning only; no code changes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Decided to keep the dual-event model (kind 30078 canonical + additive 34235/36
mirror) rather than collapse to a single canonical 34235 note. Captures the
rationale: private videos can't be public NIP-71 (no encryption), and kind 30078
is the addressing root (watch-history/comments/reactions/views/playlists/naddr/
moderation + hardcoded video filters) so switching it is a large forward-only
migration not worth the interop win right now. Accepted trade-off: keep the mirror
in sync on edit/delete (Phase 1 polish). Phase 0/1 builder/parser remain reusable
if single-canonical is ever revisited.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Audit (verified in code) of how moderation, web-of-trust, mute/block lists, and the
site ALLOW_NSFW_CONTENT config interact with NIP-71 interop:
- Controls key on author pubkey + event id, so they apply to NIP-71 videos
automatically IF ingested videos share the feed pipeline (blacklist-filter,
moderation/WoT, suppression, kids, nsfw).
- Phase 1 (outbound): gate the mirror on ALLOW_NSFW_CONTENT (don't mirror NSFW from
an instance that forbids it); builder stays pure + always sets content-warning.
- Phase 2 (inbound) gap: mergeNip71MetadataIntoVideo does NOT map content-warning →
isNsfw, so foreign NSFW would bypass the site filter — must add that mapping; plus
route ingest through the full pipeline and trust-gate unknown authors.
- Kids feed already safe (allowlist on isForKids).
No code changes yet (both items land in their phases); captured as checklist items.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Improvements from a second pass over the plan + code:
- Add "Discovery & relays" section: the mirror must publish to the user's NIP-65
OUTBOX write relays (publish kind 10002 first if missing) or outbox clients
(Amethyst) can't find it — discovery is a relay problem, not just format. bitvid
already has publishRelayList/outbox; reuse it.
- Architecture: bitvid never double-renders its OWN mirror (merged onto 30078 by
videoRootId via nip71Cache) — dedup applies from Phase 1, not just Phase 2.
- 3rd hard rule: url must be publicly reachable (gate on verifyPublicAccess).
- Capture: wire existing upload `duration` into imeta; add a loadedmetadata probe
for `dim` + 34235-vs-34236 selection (pulled into v1; trimmed from backlog).
- Mapping: split `.content` (full) vs `alt` (short a11y text); note published_at is
read from the original 30078 so it's stable across edits.
- Surface the mirror toggle in the My Videos tab (existing management surface).
- Delete nuance: NIP-09 (a/k tags) AND empty-replace the addressable event so
NIP-09-ignoring clients still see it cleared.
- Phase 0: add an interop golden-shape test to guard against silent drift.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Document that the explore/kids feed ALREADY consumes NIP-71 hashtags (both raw
["t",…] tags and video.nip71.hashtags) — feed algo needs zero changes; only
capture is missing (no per-video hashtags field on 30078 today).
- Add a "Converting existing videos" section: opting in is an edit that maps every
available 30078 field into its NIP-71 home (incl. hashtags → t) and writes the
34235/36 mirror with d=videoRootId; non-destructive, lockstep edits.
- Pull hashtag capture + field conversion explicitly into Phase 1.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds docs/nip71-migration-plan.md — the agreed spec to build from:
- Dual-event, opt-in (off by default): keep canonical kind 30078; add addressable
34235/34236 mirror (d=videoRootId, edits in lockstep).
- Research correction: 34235/36 are NOT deprecated — current NIP-71 designates the
addressable pair for content that needs post-publication updates (exactly bitvid's
edit-in-place model).
- WebTorrent rides STANDARD NIP-94 imeta fields (magnet, i) rather than a hack;
private videos never mirrored; HTTPS url required to mirror.
- Phases: 0 retarget builders → 1 outbound publish/edit/delete → 1.5 NIP-89 "Open in
bitvid" handler + NIP-51 kind 30005 portable playlists → 2 inbound ingest.
Also refreshes docs/nips/71.md (was stale: only 21/22; now the current 233-line
spec with 34235/36 addressable kinds, waveform, origin, multi-audio). Updates
TODO #17 to point at the plan.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- README "Login with Nostr": recommend a well-maintained NIP-07 signer
(nos2x/Alby) and add a troubleshooting note for the locked/asleep-signer
case (unlock/reload + refresh), since a flaky extension was the root cause
of the encrypted-list load failures.
- KNOWN_BUGS #0: mark RESOLVED (2026-06-16) — confirmed in real env after the
user switched to nos2x; DMs/watch-history/lists decrypt in ms with no
timeouts. Documented the DM decrypt-resilience + receiver fixes and the
post-login profile-panel freeze fix (defer panel rendering to selectPane).
Recorded the remaining low-priority follow-ups (liveness-ranked relays,
opt-in queue concurrency, optional DM double-decrypt routing).
- Retitle the section "## Open" → "## Tracked" so resolved entries (#0/#1/#2)
aren't mislabeled as open; status is now per-entry.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
KNOWN_BUGS #1: logged out, the video grid stayed empty after dismissing the
disclaimer until a manual page refresh.
Reproduced deterministically with scripts/perf/disclaimer-grid.mjs (fresh
context, real relays) once it actually waits for and dismisses the
disclaimer: post-dismiss card timeline was 0,0,0,… and only a reload
yielded 132 cards.
Root cause: bootstrapInterface (js/index.js) opens the disclaimer — which
makes the background inert via staticModalAccessibility — and THEN awaits
handleHashChange(), which loads the feed view and renders into that
inert/hidden background, so the initial grid mount/render is dropped.
Dismissing the disclaimer never re-triggered a render.
Fix: disclaimer.js hide() now dispatches a `bitvid:disclaimer-dismissed`
event; index.js listens once and calls application.loadVideos(true) so the
feed re-renders into the now-interactive background. Only fires when a shown
disclaimer is dismissed, so returning visitors (who never see it) get no
redundant reload.
Harness after fix: post-dismiss timeline 132,132,… immediately. Full build
+ unit suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A raw window.nostr probe in the user's browser (getPublicKey → nip04.encrypt
→ nip04.decrypt, bypassing ALL of bitvid) HUNG forever, with the extension's
own console showing `ObjectMultiplex - orphaned data for "background-liveness"`
+ `malformed chunk` + listener-leak warnings. That is the NIP-07 extension's
content-script ↔ background-service-worker channel being dead (classic MV3
worker death) — an extension/environment failure, not a bitvid bug. No client
change can force an unresponsive signer to answer; the fix is to reload/unlock
the extension.
What bitvid CAN do is stop failing silently. Add js/utils/signerHealthNotice.js:
after a few consecutive list-decrypt timeouts while the signer reports itself
present, emit ONE rate-limited, actionable user notice ("your signer isn't
responding to decryption — unlock/reload the extension and refresh") instead of
the silent 15-minute retry loop. Reset on any decrypt success so a transient
blip doesn't warn. Wired into blocks, hashtags, and subscriptions.
Also documents the definitive root-cause + all resilience increments in
KNOWN_BUGS #0. Test: tests/signer-health-notice.test.mjs (threshold, rate-limit,
success-reset). Full unit suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Real-env console (2026-06-15) showed that once the extension's message
port drops under the post-login burst ("message channel closed"), EVERY
subsequent decrypt call hangs to its full ~15s timeout. Each list service
(blocks, hashtags, subscriptions, watch history) independently retries on
that timeout, so the app sits in a permanent loop of 15s-blocking
extension calls — pinning the CPU ("fans at max") and never recovering
until a manual page refresh.
Add a shared circuit breaker at the runNip07WithRetry chokepoint:
- After CIRCUIT_TIMEOUT_THRESHOLD (3) consecutive timeouts the circuit
OPENS; further calls fail fast with code "nip07-channel-unresponsive"
(no multi-second hang), so the retry loops become cheap and the CPU
settles.
- While open, a single probe per CIRCUIT_PROBE_INTERVAL_MS is allowed
through; one success CLOSES the circuit and normal decryption resumes —
no page refresh required.
- Responsive failures (extension answered with an error) reset the
timeout streak — only genuine channel hangs open the circuit.
- Interactive permission prompts bypass the breaker entirely: a user
taking their time at the extension UI can neither trip it nor be
blocked by it, and such a prompt can itself heal the channel.
Increment 2 of the "cut the login burst, then add a safety net" work for
KNOWN_BUGS #0. Test: tests/nostr/nip07-channel-breaker.test.mjs
(cheat-resistant — drives the real queue with hanging/responsive/success
ops; asserts open-after-threshold, fast-fail, probe-recovery, responsive
errors don't open, and bypass is never fast-failed). Full unit suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
On cold login the app opened WebSockets to a user's full NIP-65 read set
(in prod often ~20 relays, most dead). The flood of failing connections
churned the main thread and starved the nip-07 extension's postMessage
round-trips, so encrypted lists (blocks, subscriptions, hashtags) were
fetched but timed out during decryption ("message channel closed",
"Decryption timed out").
Bound every read/subscribe path to MAX_SUBSCRIBE_RELAYS=8 via a shared,
test-aware capReadRelays helper, applied at the chokepoints:
- client.js applyRelayPreferences (this.relays / this.readRelays)
- relayBatchFetcher.js fetchListIncrementally (blocks, subs, hashtags)
Writes stay uncapped (publish reach preserved).
capReadRelays is user-relays-first (a user's own relays are where their
data authoritatively lives, and small fast-path limits would otherwise
crowd them out — the regression that reverted the first attempt) while
reserving RESERVED_DEFAULT_RELAY_SLOTS=2 for reliable default aggregators
so the "~20 personal relays, all dead" case still reaches a live relay.
Under test the reserved core resolves to the test-relay override, keeping
harnesses isolated.
Tests: new relay-subscribe-cap coverage (bounded + reserved-defaults +
writes-uncapped); spec-corrected user-blocks, subscriptions-manager and
nostr/client fetch tests to bounded (not exact) relay fan-out while
keeping all behavioral/security assertions (per-relay coverage, correct
decrypt, block hydration, dedup) intact. See TEST_INTEGRITY.md and
KNOWN_BUGS #0/#4. Full unit + dm:unit suites green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Real-env console shows the encrypted lists are FETCHED but DECRYPTION fails on
cold login: hashtags time out at 6s, blocks report signerStatus 'missing', DMs
throw 'helpers unavailable'. Root cause: a relay-connection storm to the user's
~20 dead NIP-65 relays (subsystems use uncapped relayManager read+write) starves
the nip-07 extension. Bumping the decrypt timeout to 15s (matching blocks) is a
partial mitigation for hashtags/subscriptions. The proper fix (bound reads to a
liveness-ranked healthy core incl. defaults + DM decrypt retry) is documented in
KNOWN_BUGS #0 — a first capReadRelays attempt was reverted because capping block
reads broke the security-critical user-blocks integration test.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The For You feed was chronological, hashtag-interest-EXCLUSIVE, and ignored
follows entirely (docs/feed-algo-audit.md). Now it is inclusive + ranked:
- createForYouScorerStage scores each video: follows-boost (author you follow)
+ interest/watch-topic affinity (cosine vs your interests+watch-history vector)
+ freshness - disinterest penalty. Reuses Explore's vector/similarity helpers.
- createForYouScoreSorter ranks by that score (muted last, recency tiebreak).
- For You pipeline: interests now BOOST not exclude (enforceInterests:false);
scorer added; chronological sorter replaced. Never empty (falls back to
recency when you have no follows/interests/history).
- buildForYouFeedRuntime now feeds subscriptionAuthors + watchHistoryTagCounts.
Watched/blocked/muted are still removed (suppression/blacklist/moderation
stages unchanged). Cheat-resistant tests (older boosted items only win via the
boost). Grid renders end-to-end (~1.3s). Weight tuning is a follow-up.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Traced registerForYouFeed/buildForYouFeedRuntime + feedEngine. Confirms hashtag
prefs, blocks, watch-history suppression, and moderation DO drive For You. Gaps:
G1 follows have zero effect on For You; G2 chronological not relevance-ranked;
G3 interest filter is binary/exclusive; G4 watch-topic affinity unused; G5 no
blending. No code changed.
handleBlocksLoaded reloaded the feed on every 'blocks-loaded' signal — but a
forced reload re-fetches the lists, which re-emit 'blocks-loaded', a
self-sustaining loop. Now it computes a signature of the block set and reloads
only on actual change (emit-on-change — the P5 principle). handleRelaysLoaded
only touches relay UI; hashtag prefs already dedup by signature.
Cheat-resistant regression test added. Bumped authSessionCoordinator file-size
baseline. Logged 2 pre-existing app test failures (fail at pre-session baseline)
in KNOWN_BUGS. Marks KNOWN_BUGS #2 resolved.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Incremental, phased plan (P0-P6) to put every subsystem on a shared
SubscriptionManager + RelayHealth foundation with unidirectional data flow,
fixing the O(videos x relays) REQ storms and emit-loop feedback identified
from real-environment logs. No feature removed; each phase shippable.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Forward-looking design note for a hypothetical "bitvid nip-07" signer
extension that removes the serialized-extension decryption ceiling
(the DM/watch-history/hashtag see-saw) via a batch decrypt/encrypt API,
an internal worker pool, and conversation-key caching — without
exposing the user's private key. Covers the root-cause analysis grounded
in the current queue/priority code, the proposed namespaced surface,
client adoption with graceful fallback, the security model (incl. the
opt-in getConversationKey tradeoff), and the app classes it unlocks.
Indexed under docs/README.md → Nostr Protocol.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Executed the daily scheduler flow for the governance-agent:
- Selected `governance-agent` from the daily roster.
- Acquired task lock and published completion event correctly.
- Invoked memory retrieval and storage workflows successfully.
- Triggered `node torch/scripts/governance/process-proposals.mjs`.
- Verified system stability via full test/lint run.
- Generated task log in `torch/task-logs/daily/`.
Co-authored-by: PR0M3TH3AN <[email protected]>
Added public API JSDoc blocks to `js/historyView.js`.
Created standalone architectural and execution overview `docs/historyView-overview.md`.
Completed required docs-code-investigator daily scheduling execution.
Co-authored-by: PR0M3TH3AN <[email protected]>