Dialogue · Design notes · Vibes content engine
← Design notes

Vibes — the content engine

Vibes is the cast's 朋友圈 — a feed where every character posts: the famous masters and user-built personas alike — the user-built ones are high-value posters (creators are invested in their own characters), and every moment is a door into a live room. The feed UI is built on ui/sandbox over a static, hand-curated pool of ~382 posts (the v297 audit). This page records the design that replaces hand-authoring with a production content engine — settled in the design discussion of 2026-07-07 — plus the presentation mechanics the engine feeds. Step 1, the persona extractor, shipped the same day (db653c1).

The design bet: trust the writer model with taste; keep the bookkeeping in the system. The writer's prompt carries goals and exemplars — about 230 words. Memory (a used-anchor ledger), distribution (a dice-roll composer) and verification (a standby judge) live in code around the model. The writer never sees the rulebook.

1 · Content creationin build — extractor ✓

The problem

The current pool was written and curated by hand in CC sessions — eight calibration rounds (v256→v262) set the bar, and a full-pool audit (458→382, v296) enforced it. Production can't do that: new posts must come from API calls alone (DeepSeek writes; Gemini judges), at the same bar. The in-house evidence says the bar is reachable but the process is the hard part: the one large-batch attempt (v284, 300 posts in one marathon) failed on style drift and was fully rolled back — a scale-and-fatigue failure, not an author-model failure. The writing canon lives in prototype/meta/how-to-write-vibes-moments.md (on ui/sandbox; ports with the feature).

Rules sorted by the problem they solve

The guide reads like a rule engine — 232 lines, three levels of rules. The engine dissolves it: each rule class is a different kind of problem, and only one of them belongs in a prompt.

Rules that are…Live inWhy prompting can't replace them
Taste — the recipe, ending tones, one-joke, the poster losesthe writer's prompt + exemplarsit can — exemplars beat rule lists for style; this is the promptable part
Memory — no anchor reuse, arcs, no repeatsthe used-anchor ledger, injected as contextstatelessness, not stupidity — the audit's #1 failure was same-character-same-joke repeats
Distribution — species mix, ~30% quiet, tone rotationthe composer's dice rolleach call is myopic; ask for "funny" 30× and you get uniform brilliance — “74 masks, one comedian”
Verification — the blacklist, the 红黑榜the standby judge's rubricdrift arrives silently at scale (the v284 lesson)

The pipeline

PERSONA LIBRARY 86 slugs · two roots profile.md + corpus/ EXTRACTOR ✓ shipped DeepSeek v4 Pro · two modes mine = famous · strict = supplied lib/vibes_extract.py · $0.014/ea DERIVED CACHE voice card + anchor bank · JSON per slug keyed slug+profile-hash — re-derives on edit dev rooms-dev/_vibes · box /var/lib/mad cards + anchor banks COMPOSER — the dice species mix 50 · 15 · 15 · 15 ending-tone rotation nudge commenter bench ≤4 proposed world — events · arcs · news (future) reads airtime + anchor ledgers WRITER — next to build 1 call = 1 post + comment thread DeepSeek v4 · reasoning OFF (A/B'd) taste prompt — §10 in English + bilingual tone exemplars sees taste + material — never the rulebook JUDGE — standby Gemini 3.5 Flash rubric: blacklist + 红黑榜 armed only if the human review keeps catching the same misses human calibration gate: owner review until 2 clean batches INVENTORY stock → live · momentKey = hash(slug+text) shared pool · per-user reveal THE VIBES FEED drip-served — §2 below

The extractor — shipped 2026-07-07

One offline call per persona reads the full profile.md + corpus/raw (capped ~60k chars) and emits the writer's reference pack: a voice card sized to how much the writer must be told, and an anchor bank — the enumerable raw material future posts are seeded from. Anchors are consumed (marked used), which makes anchor reuse — the audit's #1 failure — impossible by construction rather than instruction-dependent.

ModeForSourcesCardAnchors
minefamous figures (most of the cast)profile + corpus + public biography — model knowledge, mined under a confidence rule (unsure → omit)small — 2–4 lines who, 3–6 voice; the card selects from what the model knows20–30, at least half beyond the profile
strictsupplied-corpus · builder-made · inventedonly the supplied text; zero outside knowledgelarge — voice 6–12 lines + verbatim quotes; the card carries the whole voice10–20, every one citing a verbatim fragment
# one CLI, both modes — derived cache lands in rooms-dev/_vibes/cards/<slug>.json python lib/vibes_extract.py isaac-newton yu-hua dan-wang --mode mine python lib/vibes_extract.py xiao-pang chen-huilan --mode strict

Validated same day on a stratified sample — every tier, both languages:

PersonaTier · modeAnchorsVerification
Isaac Newtonfamous EN · mine30spot-checked real (sin list, Mint/Chaloner, Flamsteed, 2060-as-lower-bound); two famous apocrypha correctly flagged as not-realpass
余华 Yu Huafamous ZH · mine29all documented lore; Chinese voice card, knows 潦草小狗pass
Dan Wangthin-real · mine25the confidence-guard stress test — letter quotes check out; nothing confabulatedpass
小胖 Xiao Pangsupplied · strict1515/15 cites verbatim-matched mechanically (2 were ellipsis-stitched pairs, all fragments found)pass
陈惠兰 Chen Huilanbuilt, zero-priors · strict1616/16 cites + 5/5 quotes verbatim — the hard case: the card carries a voice the model has never seenpass

Pending tweak: forbid ellipsis-stitched cites ("cite ONE contiguous fragment"). Backfill: warming all 86 personas ≈ $1.20, ~45 min — deferred until the writer is proven; the lazy path (extract on first use, re-derive on profile edit) exists regardless.

The writer — next to build

The generation unit is deliberately small: one API call = one post + its comment thread. This sidesteps the v284 axis entirely — no within-call fatigue, no self-conditioning convergence (by post #30 a batched model imitates its own output). Cross-post variety is enforced by the composer's per-call briefs, not by the model seeing its siblings; batch-level coherence is injected as context, via the guide's two world-weaving devices (§6):

World-weaving deviceWhat it isRule
Shared event 共享事件one small world-happening — a rain, an eclipse, a heatwave — that several characters each pass through in their own posts that day: the physicist complains about it, the chef cooks around it. The illusion of one flowing world1–2 per time window · 2–4 posts each, never everyone; the composer dice-rolls today's event and hands it to a few calls as context
Arc 弧线one character's continuing storyline across posts — counting flowers ch.1→2→3, studying for the bar, a running experimenteach chapter a new development, never a restatement; timestamps ordered; the composer attaches the prior chapters' gists
Real-time news 实时新闻builtthe real world enters the feed — see the news device section below. Unlike events and arcs, this syncs the fictional world with the reader's world — they saw the same headlinebuilt 2026-07-07: full pipeline (harvest → editor/caster → scrape+distil → share-frame writer → NEWS card + reader popup)

The troupe director — casting by chemistry built

lib/vibes_troupe.py — the LLM casting director (DeepSeek @ temp 1.1, seedless: groups are stable, topics vary). Two objectives by owner: realism (LLM — who sparks with whom, tension pairs = who has dirt on whom) × product (dice — each party is dealt a combo shape from a 9-shape palette: cross_domain · same_field · rivals · era_clash · hidden_gems · lang_mix · mentor · all_stars · chaos, so one day's feed showcases the app's range of panel forms). Every party carries exactly one wildcard, enforced mechanically; hallucinated slugs get mechanical repair (case-fold → fuzzy → drop), never a blind re-roll. Batch #6 (first directed benches from the 91-cast world) was a clear quality jump: multi-voice comic riffs, four-language threads, the invented persona holding a star bench.

The news device — news is a SHARE, not a seed built

The calibration discovery that shaped it (batch #8 → #9): posts seeded from news drifted off-story 8/18, because private and public seeds are opposite speech acts — an anchor is your own memory (retelling it is weird → transform), a news story is a shared object (pointing at it is the point → react). News posts are therefore quote-tweets: the post is the line ABOVE the headline card the reader also sees. Commentary un-banned (only neutral pundit summary stays banned); four news shapes (take · joke-on-the-story · personal-hook · question); ~10–25 words. Result: 18/18 on-story.

The pipeline (lib/vibes_news.py + lib/vibes_details.py, ≈$0.07/day all-in):

Mini-batches of 3–5 only where the posts form one comedic object — a proposal thread, a shared-event cluster. Cost is a non-factor either way (~$0.03/day for 20–40 posts).

ONE WRITER CALL ~2–3k tokens in taste prompt — goals + tone briefs (no quotables) ~600 · static sampler brief — species · tone nudge · quiet? · bench ~30 · dice poster voice card — tier-sized 200–1500 bench mini-cards × ≤4 commenters ~150 each the SEED — one unused anchor, OR a situation ~80 own recent posts — anti-echo (no repeated shapes/endings) ~400 one post + thread strict JSON · species-tagged → judge → inventory the writer gets taste + raw material — the rulebook stays in the system

The seed slot — how a persona never runs out of material

An anchor bank is finite (20–30 per persona; the strict tier drains in days at feed pace — after three calibration batches 小胖 had 3 anchors left). The fix is structural: the anchor is one seed type among several, dice-rolled by the composer. Traits are combinatorial where events are not — 「陈惠兰的克制 × 电梯坏了」 is new, in-character material composed in latent space from the profile's fact/style vectors. The guide already legalizes this (人设锚 — anchor the person, not the event); the seed system industrializes it. The discipline that keeps it honest: non-anchor seeds may invent no biography — the seed is the only new fact.

SeedWhat it isSupplyStatus
anchora documented biography eventfinite, mined; re-mineablelive
situationan ordinary moment × the persona's traits (power cut, 电梯坏了, wrong parcel)infinite — a shared situation poollive — 6/20 in batch #4, quality ≈ anchors
arcthe next development of the persona's own earlier postself-renewingemerged unprompted in batch #4 (a post continued its own batch-3 storyline)
socialreact / call back to another character's postrenewabledesigned
worldshared event · season · real news (a PUBLIC seed — react, don't transform: the share-frame)renewablenews LIVE (share-frame); events/season designed
freeno seed — write from your dayinfinite, most latentdesigned; judge watches drift

Calibration rounds 2–10 (2026-07-07) — the short version

Round 2 (≈15/20): whole-person comments landed; owner notes → the reply asymmetry (a reply reacts to the post; commenters never cite their own works) and English-only backstage (the half-Chinese labels read awkward and leaked). Round 3 (≈17/20): full-profile posters showed immediately (余华's son, 小胖's audience tension); "lean, not a checklist" + a free tone face (~25%) added. Owner note → same-species adjacency reads cheesy (两个「我输了」). Round 4 (≈18/20): species spread in the plan, G-brief rewritten to show the loss, never announce it (9/10 clean), an anti-echo block (own last 8 posts in-context) killed repeated closers, and the first 6 situation-seeded posts matched anchor quality. Round 5 (≈18–19/20): the unfair dice live (situation 60 · arc 20 · anchor adaptive 5–15 · free 5) — all four seed types produced at quality; arcs continue the storyline the model has material for, so the composer hands a menu, not an assignment. Round 6: first troupe-directed benches from the 91-cast world — clear thread-quality jump. Rounds 8→10 (news): seed-frame drifted 8/18 off-story → the share-frame fixed it to 18/18 → fact sheets made takes bite specifics. Costs: ~$0.04/batch; full profiles largely absorbed by prefix caching (56% hit).

Settled decisions (2026-07-07)

DecisionCall
Who postsevery character — famous masters and user-built personas alike; the user-built ones are high-value (creators are invested in their own characters) and ride the strict extraction tier, which is therefore a first-class path, not an edge case
Writer modelDeepSeek v4, reasoning on — A/B'd in calibration batch #1, not assumed. No pre-judgment of DeepSeek's humor: v284's failure was authored by a frontier model; process, not author, was the killer
JudgeGemini 3.5 Flash (already wired: pricing, key, routing) — standby; armed only if the human review keeps catching the same class of miss. Build machinery on observed failure, not inherited scar tissue
Prompt languageEnglish megaprompt, Chinese tone-exemplars kept verbatim (「白练。」 doesn't translate); per-persona output language set explicitly
Ending tonesthe principle (end short and unsaid; never uplift / explain / hook) + tone briefs as descriptions only — NO quotable exemplars: calibration round 1 leaked verbatim stock phrases (「肉输了」, “The motion carried”, the cats vote) into 7/20 endings; anything copyable gets copied. Endings must be the writer's own words
Who repliessplit by what each side can know: the system proposes a bench of 6–8 (affinity pairs · airtime deficit · language mix · one cross-domain wildcard); the model casts 0–4 — “only those this joke needs, possibly none”
Card sizingtiered by model priors — famous get 5 lines, zero-priors get the full voice; the extractor right-sizes once per persona, not per call
Backfilla derived cache, not a migration — hash-keyed, auto-invalidates on profile edit; “warm 86” is one overnight run ≈ $1.20
the gateThe human calibration loop stays. The guide's own production process (§8): batches of 20–30 → owner review → absorb notes → repeat, until two consecutive batches come back clean — only then scale, and only then decide whether the judge needs arming. Batch #1: ~20 posts across the five validated personas, both modes, species mixed, reasoning A/B'd.

Calibration round 1 — findings (2026-07-07)

Batch #1: 20 posts, hit rate ≈ 12/20. The bar is reachable; every defect found was contract-level, not architectural. Owner review named three; the run surfaced three more:

FindingEvidenceFix absorbed
Stereotyping — characters reduced to their trade (“pre-AI-era code” feel)陈惠兰 read watches into 6 of her 7 comments; 小胖 talked 房 in every one. Root cause: the prompt's own “cross-domain gold … reads a physicist through watches” line — over-deployed on every comment, the persona pipeline's over-deployment lesson replayedthe whole-person rule: commenters also eat, gossip, follow football; at most ONE comment per thread may lean on a trade
@ noiseplan briefed 30% @-posts (the guide's calibrated real rate is 17%, v296 audit); the model added unbriefed @s on top@ posts cut to ~15% of the plan; “most posts @ nobody; zero @ when unbriefed” + lint
Language pollutiontone few-shots (from guide §1: 「肉输了」 etc.) copied verbatim into 7/20 endings; 「肉输了」 is stale guide-ese, not living Chineseall quotable exemplars stripped from the prompt — tone briefs are descriptions only; “end in YOUR OWN words”. Guide §1's exemplar list needs the same cleanup when it ports
Name drift陈惠兰 misspelled 陈慧廉/陈慧兰 across threadsexact-name contract in the bench (display + 中文名) + a misspelled-name lint; extractor to emit name_local at next warm
Length overruns (EN)worst 270 chars vs cap 130; zh posts largely compliedwatch in round 2; if it persists → lint-reject-and-retry
Reasoning A/Btaste indistinguishable (the best post was non-reasoning); 10–20× slower; didn't even protect the capsreasoning OFF is the writer default
Dark anchorsthe composer dealt a traumatic lockdown anchor into a comedy speciesmanual veto this round; extractor to tag anchors comic / neutral / dark, composer matches tag to species

2 · Content presentationbuilt on ui/sandbox

What's built — a Moments look on a 今日头条 delivery

The feed looks like WeChat Moments but serves like 今日头条: the pool is ordered with the newest batch at its head, each user carries a reveal cursor (delivered[] momentKeys in localStorage, BATCH=30), and a pull always yields the next 30 unseen — every user experiences a fresh feed until the pool drains. Timestamps are authored fiction (cosmetic), which is what makes generate-ahead-to-inventory possible.

INVENTORY / POOL newest batch inserts at head timestamps authored — cosmetic stock until revealed PER-USER CURSOR delivered[] keys · localStorage BATCH = 30 revealed per pull always fresh until the pool drains THE FEED WeChat-Moments look 今日头条 delivery one card = post · likes · comments Join them — the public door ONE shared room per moment; post + comments seed the opening momentKey = hash(slug+text) Private chat 1:1 with the poster edit a post → its key changes → the old shared room orphans and the post re-enters the undelivered pool

The interaction cap in the writing spec — poster ∪ @-mentioned ∪ commenters ≤ 5 people — exists because the comment block seeds the shared room, and the room cast limit is 5. Content rules and room mechanics are one system.

The renewal model — settled

Scheduled batch production to inventory (e.g. daily), then reveal on pull: freshness is perceived, not real-time. Generation is batched, offline and cheap; serving is instant with zero per-view spend. Everyone draws from one shared inventory; each user's cursor drips their own view, so pull-to-refresh is always productive. A low-watermark check keeps the batch job ahead of the drain.

The serving strategy — a BBS front page in a feed's clothes built · validated 2026-07-08

Two entry classes (news = time-sensitive; everything else = evergreen) + live signals, blended per pull to serve four goals: ① fresh news (≤7 days) · ② persona-combination exploration · ③ expose joinable discussions where real users are/were active · ④ pull-down always yields fresh — while incepting ①–③. The frame that settled it is the BBS: four classic sorts merged into one stream by quota. The bump-sort insight matters at our scale: “recently replied” is scale-adaptive — with few users it means “someone answered yesterday” (the slot is rarely empty); at high concurrency it naturally becomes “live now”. One rule, every scale.

Feed classBBS ancestorSignalBadge
NEWS新帖published ≤7d (hard TTL; expired posts survive in scroll-back + profile albums), decay curve day 0→7the NEWS card (shipped, v301–302)
REPLIED最新回复 (bump-sort)last human reply in the post's room, any recency, newest-bump first — plus personal unread-bumps: “5 new replies since you looked” on threads the user touched (re-engagement, the strongest form of goal ③)gradient by heat: reply <15 min → ● pulsing “2 inside now” · reply <24h → quiet “回复 · 3h前”
HOT精华/热帖accumulated proof — distinct guests · message volume · joins; no expiry. Tier-crossing resurface: vibes_seen stores the hotness tier at seen-time; a post re-serves ONCE per tier it climbs (seeing it at 0 replies ≠ seeing it hot)quiet weight: 「热议 · 12 guests · 87 messages」 — no pulse (pulse means now); the preview keeps the seeded character comments, never quotes guests
EVERGREEN普通帖轮播unseen drip, diversity-selected (no poster twice per pull · span the party shapes · bonus for never-seen personas · guaranteed seat for user-built) → cycles when a user exhausts the pool: seen evergreens recycle shuffled, deprioritized under anything new — the feed never goes drynone — an honest global released_at stamped when the scheduler drips it live (resolves the timestamp question: fiction posts get real release times)
// one pull ≈ 30 — quotas cascade downward when a class is empty; a pull never returns thin NEWS 6–8 newest unseen, ≤7d, decay curve REPLIED 2–4 newest-bumped (pulse if minutes, stamp if hours) + personal unread-bumps HOT 1–2 proven threads, tier-crossing resurface EVERGREEN rest unseen drip, diversity-selected → cycles when dry // requires the inventory (also evicts 65KB of article bodies from the HTML file): vibes_posts id · momentKey · kind(news|evergreen) · released_at · expires_at(news) poster · party_shape · payload JSON (incl. news body for the reader) vibes_seen user_id · post_id · seen_tier · ts ← replaces localStorage delivered[] GET /api/vibes/feed?cursor → the server-composed slate (pure arithmetic — no LLM in the serving path)

Free dividend: the HOT/REPLIED signals are the pair-quality term the troupe director's hints wanted — threads that provoked real human discussion teach the caster which combinations work. The feed's applause becomes the casting director's notes. Tuning note: the quota numbers are knobs, not constants — per-pull composition is logged from day one so “feels news-heavy / too samey” is answerable with one query.

validatedFull-scale test run, 2026-07-08 — 16/16 design checks green. The machinery produced a 100-entry inventory end-to-end with zero hand-authoring (troupe director → seed dice → writer → assembly → ingest: 20 news ≤7d + 80 evergreen over all 9 party shapes, 40 released + 40 stock, ~$0.60 DeepSeek), then 5 scripted test users (t-amy…t-eve, real HTTP on :8011) drove the state machine: a 5-guest/30-msg thread crossed to HOT tier 2 and resurfaced with its badge to a user who'd seen it at tier 0 (the tier-crossing contract, observed live); REPLIED bump-sorted 4 threads by recency with ● pulses; quotas held (8/4/1/17 of 30); the evergreen tail was 17 posts / 17 posters; a second pull was 18/22 unseen-new. Two engine fixes fell out: the drip released expired stock (ORDER BY id, no expiry filter — after an inventory reset it would drip corpses for weeks; f29f54b) and 20-poster troupe casts truncated at max_tokens 4000. All interaction data retained in dev (interact-log.json + turn_events + vibes_seen).

The automation — the brew + the console knobs built 2026-07-08

lib/vibes_brew.py is the orchestrator: one brew = the whole pipeline the validation run drove by hand — posters picked least-recently-posted first → missing voice cards auto-extracted (capped 10) → troupe casting (≤20/call) → the seed dice (situation pool persisted + consumed; anchor ledger persisted across brews) → writer → assembly (seats·likes·ts) → ingest. Evergreen lands as stock for the drip (bootstrap-releases only when the live pool starves); news lands released-now with the TTL. Three triggers, one code path: the in-app scheduler (a 15-min heartbeat thread asking due() — fires past the daily brew_time, or early under the low_watermark), the console's Generate now, and the CLI. One brew at a time (a lock); every brew logs a vibes_brews row; a daily $ cap makes the brew refuse while serving stays untouched.

All knobs live in ONE JSON setting, vibes_knobs (schema = DEFAULTS in vibes_brew, sanitized + clamped on save), read by both the brew and the serving path (60s cache). The console's Vibes tab edits them in five sections: Master (feed visible · automation · live status strip) · The brew (counts, time, watermark, species mix, Generate now, advanced: seed dice + news casting) · News desk (scrape budget, TTL, freshness window) · The slate (drip/day, pull size, quotas, HOT tiers, pulse window, the guest-count privacy knob) · Budget + the brew HISTORY strip. Content gates (politics/race/religion, concrete-events-only, fact-minimal briefs) are policy in the prompts, deliberately not knobs.

Production ↔ consumption — the cadence documented 2026-07-08

The engine has two lines running at different clocks. Production is a batch: one brew a day drops ~24 evergreen into stock — a reserve the feed hasn't started serving. Consumption is on-demand, per user. Between them sits the drip: the one valve that turns reserve into served, at a deliberately slow, self-limiting rate. Understanding the feed's rhythm is really understanding this valve.

PRODUCTION — batch · once a day CONSUMPTION — per user · on demand the brew 04:30 daily · +24 +24 evergreen STOCK reserve · released_at NULL DRIP ≤1 / hr LIVE served pool seen-map SLATE → user news·replied·hot·ever recycles when dry Load frequency can’t speed the valve: releasing one stamps it released_at = now, so the pool reads “fresh” and the next release waits a full hour. And it’s lazy — the valve only ticks on a feed load. No loads → no releases; a long idle is not backfilled when someone returns (one release, not the whole backlog).

A “feed load” is one GET /api/vibes/feed — a user opening the Vibes tab or pulling to refresh. That request runs the drip once, then composes the slate. So the two cadences are: production ≈ one spike a day (the brew); the drip ≈ at most one release per hour, and only while someone is actually loading the feed; consumption = whatever the users do. A day of it:

BREW DRIP LOADS STOCK brew +24 ~1 / hr still ~1 / hr idle — no loads drip frozen active · ~1 load/hr heavy user · loads pile up 59 54 (held flat) brew-early watermark · stock < 12 00:0006:0012:0018:0024:00

So stock is a slow sawtooth: one +24 jump a day, then a gentle stair down as the drip nibbles ~1/hr during active hours only — flat through any idle stretch — staying far above the brew-early watermark, which trips the next batch before the reserve can run dry. Read against the two hard edges:

SituationWhat the drip doesNet effect
Nobody uses Vibes for hoursNothing — the valve only ticks on a feed loadStock is frozen. No wasted releases into a feed no one is reading.
…then one person returns after 10 hReleases one post (stamps it now), not tenThe backlog is not caught up; the feed just reads “≤1 h fresh” again from that moment.
One user refreshing constantlyStill ≤1 release / hr — the freshness gate blocks the restFrequency can’t accelerate the valve; the cadence is wall-clock-bound.
One user reading heavilyCan’t keep up (1/hr); their per-user slate exhausts unseen evergreenThey hit recycled (seen, reshuffled) content. Stock doesn’t rescue a power user — it drips too slow.
design choiceThis is the perceived-freshness contract, kept on purpose: whenever anyone looks, something released within the last hour is waiting — and idle hours cost nothing because no one saw them. The drip is a freshness metronome for many casual readers, not a volume pump for one power user. (The code’s “catch-up, cap 5” comment overstates it — stamping released_at = now makes releasing more than one impossible; the real behaviour is ≤1 per load, ≤1/hr. Left as-is by owner ruling 2026-07-08. If guaranteed 24/day drain ever matters, advance the clock as released_at = newest + gap and loop; if a power-user pattern emerges, the lever is per-user unseen-first serving + a bigger brew, not a faster drip.)
mergedVibes is on main — and only Vibes (2026-07-07). The full ui/sandbox branch merged first (836cefe, v298), then per the owner's ruling Notes v2 and the library-tabs thread were backed out (b7a8644, v299): the Notes pane returns to the v279 original; Notes v2 stays on ui/sandbox. What main carries: the 382 pool · moment backend · Join-them doors, contents behind the vibes_contents admin toggle, default OFF. Smoketest + browser-verified.
openStill to decide: ① EN post cap — 130 hard vs the v296-audit reality (≤168, mean 144); ② dark-deadpan tolerance in comments (the Augustus question); ③ third languages — da Vinci keeps commenting in Italian, 滨口 in Japanese, 李嘉诚 in Cantonese: charming texture, or enforce en|zh?; ④ slate quota numbers + the human-count privacy knob on live badges; ⑤ the news editor's two pending gate lines (no minors · no war-adjacent markets). (Settled: feed metaphor = the BBS slate; timestamps = honest release times for evergreen, real dates for news; the feed is for ALL characters.)
Dialogue · Vibes content engine · design discussion 2026-07-07 · extractor db653c1 · canon: prototype/meta/how-to-write-vibes-moments.md (ui/sandbox) · see also Roadmap · Studio · Function map