<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 李飞</title>
    <description>The latest articles on DEV Community by 李飞 (@tohimi1998).</description>
    <link>https://dev.to/tohimi1998</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4207532%2F38dd786e-e02f-4cee-b0d2-7de7a0446894.png</url>
      <title>DEV Community: 李飞</title>
      <link>https://dev.to/tohimi1998</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tohimi1998"/>
    <language>en</language>
    <item>
      <title>SlowPost: A Post Office for Messages You Shouldn't Send Yet</title>
      <dc:creator>李飞</dc:creator>
      <pubDate>Mon, 26 Oct 2026 02:02:28 +0000</pubDate>
      <link>https://dev.to/tohimi1998/slowpost-a-post-office-for-messages-you-shouldnt-send-yet-21b5</link>
      <guid>https://dev.to/tohimi1998/slowpost-a-post-office-for-messages-you-shouldnt-send-yet-21b5</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SlowPost&lt;/strong&gt; is a slow post office for heated messages.&lt;/p&gt;

&lt;p&gt;We've all written &lt;em&gt;that&lt;/em&gt; message: the 1 a.m. resignation letter, the reply-all&lt;br&gt;
with a little too much honesty, the text to your roommate about the dishes that&lt;br&gt;
somehow escalates into questioning their entire upbringing. You don't regret&lt;br&gt;
&lt;em&gt;writing&lt;/em&gt; it. You regret &lt;em&gt;sending&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;SlowPost changes the workflow. Instead of sending the message, you &lt;strong&gt;mail it to&lt;br&gt;
tomorrow&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Paste the draft exactly as angrily as you wrote it, plus a line of context
("to my roommate, about the dishes").&lt;/li&gt;
&lt;li&gt;An open-weight model rewrites it into a calm version — same facts, same
request, none of the character assassination.&lt;/li&gt;
&lt;li&gt;You also get a &lt;strong&gt;neutral third-party summary&lt;/strong&gt; of the dispute and a breakdown
of what &lt;em&gt;each side&lt;/em&gt; probably needs.&lt;/li&gt;
&lt;li&gt;The letter is then &lt;strong&gt;held in the post office&lt;/strong&gt;. It doesn't go anywhere. In
real mode it unlocks at 09:00 the next morning, when you can take out the
calm version and decide — with a rested brain — whether to send anything at all.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SlowPost is a sibling of my earlier project &lt;strong&gt;WeVi&lt;/strong&gt;, a private AI mediation&lt;br&gt;
space where two people talk and only share summaries both sides approve. WeVi&lt;br&gt;
mediates conversations that are already happening; SlowPost catches the message&lt;br&gt;
&lt;em&gt;before&lt;/em&gt; it becomes a conversation you'll regret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo: &lt;a href="https://slowpost.wevi.top" rel="noopener noreferrer"&gt;https://slowpost.wevi.top&lt;/a&gt;&lt;/strong&gt; — the public demo runs in demo mode:
the overnight hold is shortened to 3 minutes so you can watch a letter unlock.
(Production mode holds letters until 09:00 the next morning.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;slowpost.wevi.top&lt;/p&gt;

&lt;p&gt;SlowPost demo: a roommate rant about takeout boxes, cooled from heat 54 to 0 — calm rewrite, neutral summary, both sides' needs, and the technique cards used&lt;/p&gt;

&lt;p&gt;Code&lt;br&gt;
Full source: &lt;a href="https://github.com/awer123456/slowpost" rel="noopener noreferrer"&gt;https://github.com/awer123456/slowpost&lt;/a&gt; — TypeScript + Fastify +&lt;br&gt;
zod, no database (letters live in a small JSON store). The technique-card&lt;br&gt;
library (data/cards.json), the prompts, and the eval harness are all in the&lt;br&gt;
repo.&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;p&gt;npm install&lt;br&gt;
cp .env.example .env   # point it at any OpenAI-compatible endpoint serving open-weight models&lt;br&gt;
npm start              # the post office&lt;br&gt;
npm run eval           # reproduce every number in this post&lt;br&gt;
Every model is swappable by environment variable — run the eval and pick&lt;br&gt;
your own default.&lt;/p&gt;

&lt;p&gt;How I Built It&lt;br&gt;
The open-source stack (and why it's the whole point)&lt;br&gt;
A tool that stores your angriest, most private words has no business being&lt;br&gt;
locked into one vendor's closed API. Every model in SlowPost is open-weight:&lt;/p&gt;

&lt;p&gt;Role    Model&lt;br&gt;
Rewrite + analysis  DeepSeek-V3.2 (open weights)&lt;br&gt;
Embeddings  Qwen3-Embedding-8B&lt;br&gt;
Rerank  Qwen3-Reranker-8B&lt;br&gt;
Here they're served through SiliconFlow's OpenAI-compatible API — but open&lt;br&gt;
weights mean the same code also runs fully locally (Ollama, vLLM) with one&lt;br&gt;
environment variable changed. Hosted today, self-hosted whenever: the choice&lt;br&gt;
is the user's.&lt;/p&gt;

&lt;p&gt;Retrieval, not just vibes. SlowPost keeps a library of 17 de-escalation&lt;br&gt;
technique cards — Nonviolent Communication, Gottman's "soft startup", repair&lt;br&gt;
attempts, boundaries without threats, the overnight rule. For each draft it&lt;br&gt;
runs hybrid retrieval (BM25 + embedding recall, merged with Reciprocal Rank&lt;br&gt;
Fusion) and a cross-encoder rerank to pick the three techniques that fit&lt;br&gt;
this specific conflict. Those cards are injected into the prompt, so the&lt;br&gt;
rewrite is grounded in an explicit technique, not generic "be nicer" energy.&lt;/p&gt;

&lt;p&gt;Strict structured output. The model must return JSON — calmRewrite,&lt;br&gt;
neutralSummary, theirNeeds, myNeeds, techniquesUsed, riskFlags —&lt;br&gt;
validated against a zod schema. Invalid output is retried once with the&lt;br&gt;
validation error fed back.&lt;/p&gt;

&lt;p&gt;The hold. Letters are stored with a release time (next morning, 09:00;&lt;br&gt;
demo mode shortens it). The UI won't show the calm version until the hold&lt;br&gt;
expires. Slowness is the feature.&lt;/p&gt;

&lt;p&gt;I measured it instead of guessing&lt;br&gt;
I built a small eval harness: 12 heated Chinese drafts (roommates, partners,&lt;br&gt;
clients, landlords, a 1 a.m. breakup letter…), each labeled with the technique&lt;br&gt;
cards a human would pick.&lt;/p&gt;

&lt;p&gt;Retrieval Recall@3: 75.0% (first run: 54.2%) — strict scoring: each case&lt;br&gt;
labels the two cards a human would pick, and near-miss cards (equally valid&lt;br&gt;
techniques) count as misses. The first run's misses turned out to be mostly&lt;br&gt;
my pipeline's fault, not the reranker's: I cut the candidate pool to 8&lt;br&gt;
before reranking (the right card was often sitting at #9–14), and the&lt;br&gt;
cards' written vocabulary didn't match how people actually type when angry.&lt;br&gt;
Widening the pool, adding colloquial keywords to the cards, and phrasing the&lt;br&gt;
rerank query as a task ("pick the techniques that fit this message") took&lt;br&gt;
recall from 54.2% to 75.0%.&lt;br&gt;
Generation, per model (schema success / avg latency / avg "heat reduction"&lt;br&gt;
from a transparent lexicon metric that scores absolutes, insults, threats and&lt;br&gt;
exclamation density before vs. after; latency measured from a remote dev box,&lt;br&gt;
the in-region production server is faster):&lt;br&gt;
Model   Schema success  Latency Heat reduction&lt;br&gt;
DeepSeek-V3.2   12/12 (100%)    12.9s   83%&lt;br&gt;
DeepSeek-V4.1-Flash 12/12 (100%)    14.8s   72%&lt;br&gt;
Qwen3.5-35B-A3B 9/12 (75%)* 97.2s   78%&lt;br&gt;
DeepSeek-V3.2 won on every axis — fastest and coolest-headed — so it's the&lt;br&gt;
default. The newest model (Qwen3.5-35B-A3B) was 7× slower because of its long&lt;br&gt;
reasoning traces: a neat reminder that "latest flagship" ≠ "best fit" for a&lt;br&gt;
small, well-scoped job.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qwen3.5 was measured from the production server; 3 of its 12 calls failed
on provider errors (503s, dropped connections) across repeated attempts from
two networks, so its figures cover the 9 completed cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The eval ships in the repo (npm run eval) — run it, get your own numbers,&lt;br&gt;
pick your own default model. That's the open-source way.&lt;/p&gt;

&lt;p&gt;What I learned&lt;br&gt;
Retrieval quality is the product. My first instinct was to spend all the&lt;br&gt;
effort on the rewrite prompt. The eval disagreed: when the wrong technique&lt;br&gt;
cards were retrieved, even a perfect prompt produced generic advice. Adding the&lt;br&gt;
rerank stage changed which cards got picked for most drafts, and the rewrites&lt;br&gt;
immediately became more specific — a concrete rota for the roommate, a boundary&lt;br&gt;
script for the parents. Grounding beats eloquence.&lt;/p&gt;

&lt;p&gt;Newest ≠ best fit. I benchmarked three open-weight models expecting the&lt;br&gt;
newest flagship to win. DeepSeek-V3.2 — not the newest — was fastest and&lt;br&gt;
produced the coolest rewrites, while Qwen3.5-35B-A3B's long reasoning traces&lt;br&gt;
made it 8× slower for no quality gain. For a small, well-scoped job, a boring&lt;br&gt;
model with a strict schema beats a brilliant one thinking out loud.&lt;/p&gt;

&lt;p&gt;Define the metric before you argue about quality. "Is this rewrite good?"&lt;br&gt;
is a vibes question. Forcing myself to define heat — absolutes, insults,&lt;br&gt;
threats, exclamation density — as a dumb, transparent lexicon score made&lt;br&gt;
quality measurable and comparable across models. The metric is crude and I say&lt;br&gt;
so in the repo; a crude metric you can run beats a sophisticated opinion you&lt;br&gt;
can't.&lt;/p&gt;

&lt;p&gt;…and check your ruler before you blame the model. Midway through, a demo&lt;br&gt;
run showed a calm rewrite scoring hotter than the original draft (54 → 64).&lt;br&gt;
The rewrite was fine; the metric wasn't. It contained the word 垃圾 — which&lt;br&gt;
means both "trash" (as in "shall we take turns clearing the trash?") and&lt;br&gt;
"you're trash" — and one literal, innocent use maxed out the score. I fixed&lt;br&gt;
the lexicon (masking literal uses, replacing ambiguous single characters with&lt;br&gt;
actual insult phrases) and added a guard to the pipeline itself: if a rewrite&lt;br&gt;
doesn't measure cooler than the draft, it regenerates once and keeps the&lt;br&gt;
cooler version. The most dangerous bugs in an AI pipeline are the ones in&lt;br&gt;
your own measuring stick.&lt;/p&gt;

&lt;p&gt;Why Does Open Innovation Matter?&lt;br&gt;
SlowPost stores the angriest, most private words a person types all week.&lt;br&gt;
"A closed API is your only option" is the wrong default for that job — open&lt;br&gt;
weights are what let the user choose where those words are processed,&lt;br&gt;
inspect what processes them, and self-host the whole thing when they want to.&lt;/p&gt;

&lt;p&gt;Openness is also what made this build possible at all. In one weekend, one&lt;br&gt;
person benchmarked three open-weight models head-to-head, picked a default by&lt;br&gt;
measurement instead of marketing, and shipped — because every model in the&lt;br&gt;
stack can be swapped with an environment variable and re-evaluated with the&lt;br&gt;
harness in the repo. No contracts, no vendor lineup deciding for you.&lt;/p&gt;

&lt;p&gt;And it compounds after the hackathon: anyone can fork this, run the eval,&lt;br&gt;
beat my 75.0% recall, and publish their numbers. A closed tool gets users;&lt;br&gt;
an open one gets successors.&lt;/p&gt;

&lt;p&gt;Slow down. Send tomorrow. &lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
