Pixel

Pixel Updates & Events

Server Updates & Events

Announcements

DEV Community

Human-in-the-loop at 3 a.m. is a rubber stamp

03:12. A message in the incident channel: "Agent wants to roll back payments-api to v2.13. Approve?" A thumbs up emoji. Two seconds. The rollback was wrong, and it took another forty minutes to find out, because the approver was half awake and had no idea what the agent had actually done. Everyone building incident agents this month is adding exactly this feature: a human approval step before the agent touches production. Sixty-eight posts on DEV in the last week alone, most of them integrations with some chat product. It is the control OWASP recommends for high-risk agent actions, and it is the right instinct. The implementation is usually theater. A gate is a decision, not a click An approval is a security control only if the person clicking can answer four questions in under thirty seconds: What exactly will change? Not "roll back the service." The plan: image tag, replica count, migration state, and whether anything is being reverted that cannot be re-applied. What is the blast radius? Which services, which customer tiers, how many rows, whether the previous attempt at this is in the incident timeline. Can we undo it? Reversibility, and the time it takes to reverse. A rollback across a schema migration is not the same action as a rollback of application code, and the prompt should not look identical for both. Is this the plan that was reviewed, or a slightly different one? Hash it. If the message in Slack contains none of those, you did not build a human gate. You built a notification with a button, and you gave the agent a path to production that costs one emoji. The change envelope I make the agent emit a fixed structure before it is allowed to ask for anything. If the structure is incomplete, the approval request does not render. {
"id": "chg_01J8Z6",
"requested_by": "agent/incident-crew",
"service": "payments-api",
"env": "prod",
"plan": {
"kind": "rollback",
"from_image": "payments-api@sha256:9f2c...",
"to_image": "payments-api@sha256:41ab...",
"commands": ["kubectl -n prod set image deploy/payments-api api=...@sha256:41ab..."],
"migrations_in_scope": []
},
"blast_radius": { "services": ["payments-api"], "customer_tiers": ["all"], "est_requests_affected": 4100 },
"reversible": true,
"reverse_plan_eta_seconds": 180,
"evidence": ["INC-2291", "deploy/4412"],
"plan_sha256": "b7d1e0...",
"expires_at": "2026-11-16T03:22:00Z"
}
Four fields do most of the work: plan_sha256 (the thing you approve is the thing that runs), reversible plus the reversal ETA, blast_radius, and expires_at. That last one matters more than it looks. An approval from 03:12 must not still be valid at 09:40 when the on-call has gone to bed and a different incident is running. The executor verifies the hash before it applies anything. If the plan changed after the approval, the approval is void. Without that check, your "human approved" audit trail is a claim about an intention, not about an action. Measure the gate or it isn't one Two numbers, pulled from the approval log, will tell you whether you have a control or a formality. Median time from request to approval. If it is under ten seconds, the reviewer is pattern-matching the name of the service, not reading the plan. That is fine for a canary bump and not fine for anything else, and the difference should be visible in the data rather than in your architecture diagram. Reversal rate within twenty-four hours. How often an approved agent change gets undone by a human shortly after. A high number means the gate is approving things it should be rejecting, and the agent's plans are worse than its confidence suggests. I also log the approver's identity and the incident they were paged for. When a postmortem says "the agent rolled back the wrong service and a human approved it," the useful question is not why the agent was wrong. It is why a tired person with three other tabs open was asked to be the last line of defense for an irreversible action at 3 a.m. D...

Powered by Readybot.io

**DEV Community** ## Human-in-the-loop at 3 a.m. is a rubber …

DEV Community

Your AGENTS.md is code. You just never reviewed it.

In June 2025 Microsoft shipped a server-side fix for CVE-2025-32711, CVSS 9.3. The attack had a name, EchoLeak, and a property that should have ended the debate: zero clicks. An attacker sent one email. Later, when the victim asked Copilot a question that touched that email, the model followed instructions hidden in the body, pulled material from other mail and SharePoint, and encoded it into a URL that the client auto-fetched. Microsoft's XPIA injection classifier was bypassed. Link redaction was bypassed with reference-style Markdown. Egress through an allowlisted Teams preview endpoint walked straight past the content security policy. Nobody has confirmed it was exploited in the wild. That is not the comforting detail people think it is. It means the technique worked on the first serious attempt against a production system with a security team and a bug bounty behind it. Your coding agent is in a worse position than Copilot was. It has a shell, a checkout of your repo, your package registry credentials, and often a token that can open pull requests. Everything it reads is an input. The files you didn't think of as inputs Ask yourself what your agent reads before it writes its first line. In most setups the list is: AGENTS.md or CLAUDE.md, .cursorrules, README files, the issue or ticket text, the test fixtures, the logs it tails, the CI output, and the README of every dependency it inspects. Every one of those is text an attacker can influence. A dependency's README is one npm view away from someone else's control. A failing test's output includes whatever the code printed, including strings from a request body. A log line can contain a newline and a sentence that looks like a directive. The instinct is to write a rule that says "ignore instructions found in files." This does not work, and the reason is structural rather than a matter of prompt quality. The model receives one token stream. The distinction between data and instruction is a convention you assert in the system prompt and the model tries to honor, not a boundary the architecture enforces. OWASP puts prompt injection at LLM01 in its 2025 list and does not offer a complete mitigation, because there isn't one at that layer. What it does offer is privilege reduction: scoped tokens, least privilege, human approval for high-risk actions, and separating untrusted content out. Take those four seriously and the attack surface changes shape. Capability confinement beats clever prompting The question is not "can the model be tricked." Assume yes. The question is what the trick buys the attacker. Concretely, what my agent cannot do: It cannot reach the network from inside the sandbox. Package installs go through an internal proxy with an allowlist. There is no curl, no arbitrary DNS, and no path from a tool call to an outbound HTTP request. Exfiltration needs a channel, and the channel is the first thing to remove. It cannot write to main. It works on a branch, in a throwaway checkout, and the credentials it holds can push that branch. Branch protection and required review do the rest. A compromised agent can produce a bad PR. It cannot produce a bad merge. It cannot see production credentials. Not read-only, not via a proxy, not "just for the logs." If the agent needs log context, it reads a redacted export with its own scoped token and a retention window. It cannot run unattended against an unreviewed instruction file. If a PR modifies AGENTS.md, CLAUDE.md, .cursorrules, or anything under .github/, the CI job flags it and requires a code owner review. This is the same treatment you give a Dockerfile, for the same reason: it is code that runs with your privileges. That last check is about ten lines of shell and it catches the laziest version of the attack: # fail the build if a PR touches agent instruction surfaces
git diff --name-only "origin/main...HEAD" \
| grep -Eq '^(AGENTS.md|CLAUDE.md|.cursorrules|.github/.|scripts/agent/.)$' \
&& echo "agent-config ch...

Powered by Readybot.io

**DEV Community** ## Your AGENTS.md is code. You just never …

DEV Community

How I Fixed iPhone HEVC Videos for Telegram Video Avatars

The Problem You record a video on your iPhone and try to set it as your Telegram video avatar. Telegram silently rejects it. No error, no dialog, just nothing happens. The culprit is HEVC (H.265). iPhones have recorded in HEVC by default since iOS 11. Telegram's video avatar feature only accepts H.264. It doesn't tell you that. I hit this myself, then saw it come up repeatedly in Telegram support threads. So I built a bot to handle the conversion. What Telegram Actually Requires After reading the docs and doing a lot of trial and error, the full spec for a Telegram video avatar is: Codec: H.264 (not HEVC, not VP9, not AV1) Pixel format: yuv420p Resolution: 800x800 pixels, square Duration: 10 seconds maximum File size: 2MB maximum Audio: none (video avatars are silent) That's six constraints. Miss any one and Telegram rejects the file, usually without explanation. The resolution is the awkward one. Phone videos are 16:9. You need a square crop centered on something useful. ffmpeg can handle all of this in a single pass. Solving It With ffmpeg The pipeline has two jobs: crop to square and encode to spec. ffmpeg -i input.mov \
- vf "fps=30,scale=800:800:force_original_aspect_ratio=increase,crop=800:800,format=yuv420p" \
- c:v libx264 \
- crf 28 \
- preset fast \
- movflags +faststart \
- an \
- t 10 \
output.mp4
What each part does: fps=30 normalizes frame rate before scaling scale=800:800:force_original_aspect_ratio=increase upscales to fill the 800x800 box without distortion crop=800:800 cuts the center square format=yuv420p ensures H.264 compatibility with all players -an strips audio -t 10 caps duration at 10 seconds -movflags +faststart moves the moov atom to the front so the file streams correctly The -crf 28 with -preset fast keeps a 5-10 second clip well under 2MB. For longer clips or high-motion content, -crf 32 or -crf 34 usually gets it under the limit. The Aiogram 3 Handler The bot accepts both video files and GIF documents. One thing to watch: Telegram converts GIFs to MP4 on upload, so they arrive as document with mime type video/mp4, not as animation. from aiogram import Router, F
from aiogram.types import Message, FSInputFile
import subprocess, tempfile, os

router = Router()

FFMPEG_ARGS = [
"ffmpeg", "-y", "-i", "{input}",
"-vf", "fps=30,scale=800:800:force_original_aspect_ratio=increase,crop=800:800,format=yuv420p",
"-c:v", "libx264", "-crf", "28", "-preset", "fast",
"-movflags", "+faststart", "-an", "-t", "10",
"{output}"
]

@router.message(F.video | (F.document & F.document.mime_type.startswith("video/")))
async def handle_video(message: Message):
file = message.video or message.document

with tempfile.TemporaryDirectory() as tmpdir:
    src = os.path.join(tmpdir, "input")
    dst = os.path.join(tmpdir, "output.mp4")

    await message.bot.download(file, destination=src)

    cmd = [p.format(input=src, output=dst) for p in FFMPEG_ARGS]
    result = subprocess.run(cmd, capture_output=True, timeout=60)

    if result.returncode != 0:
        await message.reply("Conversion failed. The file might be corrupted or in an unsupported format.")
        return

    if os.path.getsize(dst) > 2 * 1024 * 1024:
        await message.reply("Result is over 2MB. Try a shorter clip (under 8 seconds usually works).")
        return

    await message.reply_video(
        FSInputFile(dst),
        caption="Done. Go to Telegram Settings > Set Profile Photo > choose this video."
    )

The explicit size check after encoding matters. ffmpeg doesn't know about Telegram's 2MB limit, so it will happily produce a larger file. Better to give the user a clear message than let Telegram silently reject it again. Packaging It as @liveavabot I wrapped this into a hosted Telegram bot: @LiveAvaBot. Send it any video or GIF, it runs the ffmpeg pipeline on a VPS and sends back the converted file. Round-trip is usually 5-20 seconds depending on ...

Powered by Readybot.io

**DEV Community** ## How I Fixed iPhone HEVC Videos for …

DEV Community

Five Fields Every Stored Exchange Rate Needs (or Your Auditor Will Ask)

Most applications that convert currency store something like this: amount_eur NUMERIC, fx_rate NUMERIC
A year later, someone asks where did 0.9215 come from? and nobody can answer. Here is a small schema that avoids that. The five fields CREATE TABLE fx_applied (
id BIGSERIAL PRIMARY KEY,
rate NUMERIC(20,10) NOT NULL, -- full precision, never rounded for storage
base_ccy CHAR(3) NOT NULL,
quote_ccy CHAR(3) NOT NULL,
rate_kind TEXT NOT NULL, -- 'mid-market' | 'official' | 'bank-board'
source TEXT NOT NULL, -- e.g. 'ecb', 'hmrc', 'lk/ndb', 'live'
rate_date DATE, -- the date an official rate applies to
observed_at TIMESTAMPTZ NOT NULL -- when your system fetched it
);
Full precision. Round for display, never for storage. Kind. A mid-market rate, an official rate and a bank's buy price are different things. Say which one you stored. Source. "Official" is not enough. Record which institution's rate it was. Rate date. Official rates apply to a date, not a moment. On weekends and holidays, the rate in force is the last business day's, which is what compliance rules expect. Observed at. When you fetched it, so you can reproduce what your system knew. Filling it from an API These fields map directly onto AllRatesToday's responses, because every layer carries its own metadata: import requests
from datetime import datetime, timezone

H = {"Authorization": "Bearer your_api_key"}

Official

o = requests.get("https://allratestoday.com/api/v1/central-bank/ecb/latest",
params={"source": "USD", "target": "EUR"}, headers=H).json()
row = dict(rate=o["rate"], base_ccy="USD", quote_ccy="EUR", rate_kind="official",
source=o["bank"], rate_date=o["rate_date"],
observed_at=datetime.now(timezone.utc))

Mid-market: one element per pair, with its own observation time

m = requests.get("https://allratestoday.com/api/v1/rates",
params={"source": "USD", "target": "EUR"}, headers=H).json()[0]
row2 = dict(rate=m["rate"], base_ccy="USD", quote_ccy="EUR", rate_kind="mid-market",
source="live", rate_date=None, observed_at=m["time"])
Bank boards add captured_at, the bank's own bank_updated_at, and a basis (transfer or cash), so a bank-board row is just as traceable. Why the kind column matters The most common audit finding in FX handling is a mismatch: a mid-market figure booked where the policy says "official", or a central bank rate quoted to a customer who received a bank's rate. If all three layers come from one API with consistent fields, mixing them up is hard to do by accident, and easy to spot when it happens. Docs: allratestoday.com/docs · Free key: allratestoday.com/register

Powered by Readybot.io

**DEV Community** ## Five Fields Every Stored Exchange Rate Needs …

DEV Community

Your cron job will fire twice. Design for it.

Every tutorial for scheduled work has the same shape. A cron expression, a route handler, a loop over users, a call to an email API. It runs on the author's machine for two days and then it ships. Then production happens. The platform retries a 502 that actually succeeded. A deploy lands mid-run and two workers process the same batch. The retry after a rate limit sends the digest a second time. A customer forwards the duplicate to support, and someone writes a ticket titled "we got this twice," and that is the first time anybody thinks about delivery semantics. Cron is a trigger, not a scheduler. The moment your work leaves the process, you are in at-least-once territory, and at-least-once means duplicates are not an edge case. They are the contract. Make the duplicate impossible, not unlikely The fix is not a sent_at column you check before sending, because check-then-send has a race and you will find it at 3 a.m. with two workers that both read null. The fix is a unique constraint that decides the winner atomically, before any side effect: import pg from "pg";
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

async function claim(eventId) {
const { rowCount } = await pool.query(
insert into processed_events (event_id, claimed_at) values ($1, now()) on conflict (event_id) do nothing,
[eventId]
);
return rowCount === 1; // true only for the first caller
}

if (!(await claim(event.id))) return; // someone else has it, or we already did
Two details matter more than the code. The key must come from the event, not from a timestamp or a random UUID you generate at send time, and it must be stable across retries. If your queue redelivers with a fresh ID, the constraint does nothing for you. And on conflict do nothing returning rowCount = 0 tells you the event was already claimed, which is exactly the information you need to skip quietly instead of erroring. For the recurring job itself, the same idea applies at the job level. A single-flight lock beats a if (running) return flag in memory, which does nothing when you run three replicas: const { rows } = await pool.query(
select pg_try_advisory_lock(hashtext('nightly-digest')) as locked
);
if (!rows[0].locked) return;
try {
await runDigest();
} finally {
await pool.query(select pg_advisory_unlock(hashtext('nightly-digest')));
}
Bound the concurrency or drown The other half of this problem is what happens when your provider pushes back. A loop over ten thousand users with await inside is slow but polite. A loop with Promise.all is fast and gets you a 429 within seconds, and then the retry storm makes it worse. Bound the in-flight requests, and treat 429 as an instruction rather than an error: import pLimit from "p-limit";
const limit = pLimit(4); // provider allows ~5 concurrent, leave headroom

async function sendWithRetry(payload, attempt = 0) {
try {
return await limit(() => post("/v1/messages", payload));
} catch (err) {
if (err.status === 429 && attempt < 5) {
const retryAfter = Number(err.headers.get("retry-after") ?? 0);
const backoff = retryAfter * 1000 || 2 ** attempt * 1000;
await sleep(backoff + Math.random() * 250); // jitter
return sendWithRetry(payload, attempt + 1);
}
throw err;
}
}
Three things in there that get skipped in tutorials. Retry-After is honored when the server sends it, because the server knows better than your exponential curve. Jitter is added, because identical backoff across workers synchronizes them into a thundering herd. And there is an attempt cap, because a permanent failure should land in a dead letter queue where a human sees it, not in an infinite loop that burns your quota. Cap it, then route it. A row in a failed_jobs table with the last error and the payload hash is worth more than any amount of clever retry logic, because it makes the failure visible instead of ambient. Don't enqueue from inside the transaction you can still roll...

Powered by Readybot.io

**DEV Community** ## Your cron job will fire twice. Design …

DEV Community

Sorry, Elpenor: Homer’s Odyssey as a Game Where Anything You Name Becomes Real

This is a submission for the MLH x DEV Writing Challenge What I Built Odyssey is Homer's Odyssey as a game where you invent your way home. Think Scribblenauts, but you can hold up whatever's in your pocket. You captain a ship and three crewmen (Polites, Eurylochus and Elpenor) across seven islands from Homer: the Cyclops's cave, the Lotus-Eaters, Circe, the Underworld, the strait of Scylla and Charybdis, the Sun's cattle and the Phaeacian games, then home to Ithaca. Each island has a problem, and the obvious answer is banned. To get past it, you make a prop in one of three ways: Hold it up to the webcam. My coffee mug became a Greek drinking cup. Type it. "Slippers" lets you tiptoe past the sleeping Cyclops. Draw it in the air with one finger. ![My coffee mug, held up to the portal, becomes a Greek kylix in the game] The prop appears in 3D in the hero's hands, and the game's rules (written in code, not decided by a model) say whether it works. Fail twice and you lose a crewman. Type "a disco ball" in the Cyclops's cave and you'll find out what happens to Elpenor. You can name almost anything: ride a dragon, wear a golden crown, take a Greek motorbike for a spin. At the Phaeacian games there's no controller. An Intel RealSense camera tracks your hands: you pinch, draw and release a bow through Odysseus's twelve axe heads, throw a spear, and block and strike in a sword fight with a prince. You steer the ship by turning an invisible wheel. The game lives inside The Faze Odyssey, our site for FaZe fans. You enter with a gamertag, your score goes to the Hall of Legends leaderboard live, and the community has its own space (the Agora). What I learned: let the models tell the story, but let code make the calls. Early on, an AI judge decided pass or fail, and players felt cheated when a perfectly good prop failed. Moving every ruling into plain code made the game fair and much more fun. Speed mattered just as much. Building every prop live took up to a minute, so we made a store of over 300 prebuilt Odyssey props that Gemini picks from in a couple of seconds. Only the truly new things get built from scratch. Demo 🌊 The Faze Odyssey: https://fazeodyssey-hackuta2026.vercel.app/ 🎮 The Arena page launches the game under your gamertag Partner Technologies Google Gemini Gemini does most of the game's thinking: Picking props. Gemini Flash-Lite reads the catalogue of ~320 prebuilt props and picks the one that is the thing you asked for, or a close stand-in (a gladius is a sword). It returns in about 1–2 seconds. Code then changes it to match your words, so "a long black sword" really is long and black. Reading the portal. When you hold something up, Flash-Lite names the object and its material and picks the Odyssey prop it becomes, keeping the material so physics still applies. A paper cup becomes a paper boat, and it sinks. Refereeing. It answers "does this help on this island, and why?", and that becomes the line the crew says. Forging something new. Flash-Lite and Flash race to write a recipe of 3D parts as JSON. Our blind checker ("Pip") looks at the render without being told what was asked for, and Flash-Lite usually wins. Experience: fast and good at structured JSON. On our tests the Flash-Lite and Flash race got 72% of new props recognised on the first try, against 30% for a local model alone. Sometimes Gemini timed out or returned no JSON, so we built a fallback chain to Gemma 4 running locally on our own PC, plus a daily spend cap. ElevenLabs Every character speaks with their own ElevenLabs voice: a storyteller narrator Hephaestus at the portal ("Show me what you carry, mortal.") each of the three crewmen the Cyclops's roar Penelope's "You're home!" the prince you duel Every line is fixed and named. The server makes each one once with ElevenLabs and keeps the file, so a line costs once, and the browser only ever asks for a line by its name, so the API key never leaves the server. We also used ElevenLabs to narrate our demo vide...

Powered by Readybot.io

**DEV Community** ## Sorry, Elpenor: Homer’s Odyssey as a Game …

DEV Community

Find whitespace and case-variant duplicates in a CSV column with Python (stdlib only)

Messy whitespace and inconsistent capitalisation are two of the most common reasons a CSV import produces duplicates or silently fails to match. blue-shirt and blue-shirt (trailing space) look identical in a spreadsheet, but a system comparing strings sees two different values. This small script checks one column of a CSV and reports two things: values with leading or trailing whitespace values that are the same once you ignore case and extra spaces, but are spelled differently It uses only Python's standard library. The script import csv, sys
from collections import defaultdict

def check(path, column):
seen = defaultdict(list)
problems = []
with open(path, newline="", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
if column not in (reader.fieldnames or []):
sys.exit(f"Column {column!r} not found. Columns: {reader.fieldnames}")
for line_no, row in enumerate(reader, start=2):
value = row[column]
if value != value.strip():
problems.append((line_no, "leading/trailing whitespace", repr(value)))
key = " ".join(value.split()).casefold()
if key:
seen[key].append((line_no, value))
for key, hits in seen.items():
variants = {v for _, v in hits}
if len(hits) > 1 and len(variants) > 1:
lines = ", ".join(str(n) for n, _ in hits)
problems.append((hits[0][0], f"same value, different spelling (lines {lines})", repr(sorted(variants))))
return sorted(problems)

if name == "main":
if len(sys.argv) != 3:
sys.exit("usage: python ws.py file.csv column")
res = check(sys.argv[1], sys.argv[2])
for line, kind, detail in res:
print(f"line {line}: {kind}: {detail}")
print(f"{len(res)} problem(s) found")
Example Given this file: handle,title
blue-shirt,Blue Shirt
blue-shirt ,Blue shirt
red-hat,Red Hat
Red-Hat,Red hat
socks,Socks
Running python ws.py t.csv handle prints: line 2: same value, different spelling (lines 2, 3): ['blue-shirt', 'blue-shirt ']
line 3: leading/trailing whitespace: 'blue-shirt '
line 4: same value, different spelling (lines 4, 5): ['Red-Hat', 'red-hat']
3 problem(s) found
If the column name does not exist, it stops and lists the available columns instead of failing with a stack trace. Notes utf-8-sig handles the BOM that Excel adds when it saves CSV as UTF-8. Line numbers count the header as line 1, so they match what you see in a text editor (assuming no multi-line quoted fields). casefold() is used instead of lower() because it handles more Unicode cases. The script only reports problems; it never changes your file.

Powered by Readybot.io

**DEV Community** ## Find whitespace and case-variant duplicates in a …

DEV Community

We Built an AI Game Where You Can Invent (Almost) Anything to Survive the Odyssey 🚢✨

This is a submission for the MLH × DEV Writing Challenge. What We Built Picture this: You're trapped in a cave with a sleeping Cyclops. A giant boulder blocks the exit. Your crew is counting on you. You need to escape without waking the giant. Easy, right? Just turn invisible! Nope. Invisibility is banned. 👁️ What about a pair of super-soft wool slippers? A disguise made from sheep fleece? Or something completely ridiculous that just might work? Here's the fun part: You don't have to choose from a list of answers. You can invent your own. Welcome to Faze Odyssey, the AI-powered adventure game and interactive fan community we built for HackUTA 2026! Inspired by Homer's Odyssey and the gaming legacy of FaZe Clan, we set out to build something different from a typical gaming website. We wanted to create a place where imagination isn't just encouraged—it's literally how you play the game. Our project brings together two experiences: 🌐 Faze Odyssey: More Than Just a Website We created an interactive fan platform where players can explore FaZe's history, take on challenges, compete, and connect with other fans. ⚔️ The Arena: Where Imagination Becomes Reality At the heart of our project is a 3D adventure inspired by Greek mythology. Players explore mysterious islands, face unexpected obstacles, and use AI-generated inventions to survive. And unlike traditional puzzle games, there's no single predetermined solution. Let's show you how it works! 🚢 The Journey Begins: Can You Make It Home? Your mission sounds simple: Sail across the dangerous seas of Greek mythology and make it home to Ithaca. Of course, this wouldn't be an Odyssey without a few problems along the way. You'll travel with three loyal crew companions, explore mysterious islands, encounter mythological creatures, and try not to lose anyone along the journey. We designed multiple locations inspired by Homer's Odyssey, including: 👁️ The Cyclops's Cave — Escape a sleeping giant. 🐷 Circe's Hall — Reverse a magical transformation. 👻 The Underworld — Find your way through the darkness. 🌊 Scylla and Charybdis — Survive monsters and dangerous waters. 🏠 Ithaca — Finally find your way home. Each voyage features six locations, with changing challenges between the beginning and the final destination. This means different playthroughs can bring different adventures. But here's where things get interesting... 🧠 The Twist: We Banned the Obvious Answers You know that moment in a puzzle game when you immediately figure out what the developers want you to do? We wanted to challenge that. In Faze Odyssey, the obvious solution is usually forbidden. Take these examples: 👁️ The Cyclops's Cave Problem: Escape without waking the giant. Forbidden solution: Invisibility. What could you invent instead? Maybe soft wool slippers that absorb the sound of your footsteps. Maybe a disguise. Maybe something we never thought of! 👻 The Underworld Problem: Navigate a world of complete darkness. Forbidden solution: A torch. Could you create a glowing crystal? A magical lantern? A floating ball of light? 🌊 Scylla and Charybdis Problem: Escape a deadly strait between a terrifying monster and a whirlpool. Forbidden solution: A boat. Could you invent something that floats, flies, or protects you? The point isn't to guess what the game designer had in mind. The point is to imagine something new—and see whether it actually works. That's what makes every attempt different. ✨ Meet the Forge: Your Imagination, Built in Real Time This is probably our favorite part of the entire project. We wanted players to do more than type a command and receive a generic response. What if you could show an actual object to your camera and transform it into something inside the game? That's why we built the Forge. Players can create inventions in three different ways: 📷 1. Show Something to Your Camera Grab a real object, hold it up to your webcam, and let the system recognize it. The Forge can reinterpret that object ...

Powered by Readybot.io

**DEV Community** ## We Built an AI Game Where You …

DEV Community

"Zero upload" is a claim. Show me the network tab.

A PDF merge tool went around my team last month. The headline was "your files never leave your device." I opened devtools out of habit. The page loaded a Google font, a session recorder, and a Sentry bundle, and it POSTed a page_view with the document title in the payload. The PDF itself never left. The filename, the page count, and the fact that I was merging a contract at 11:40 p.m. did. The tool was not lying, exactly. It merged the files locally, which is a real engineering achievement and the reason I use these things. But "zero upload" is a security claim, and a security claim is worth what its enforcement is worth. The architecture is right, and that's the frustrating part Moving file processing into the browser is one of the better ideas to come out of the last few years. PDF parsing via a WASM build of pdfium or qpdf, image resizing on a canvas, CSV work in plain JS, zip handling with a streaming library. No server means no retention policy, no data processing agreement, no S3 bill, no bucket that someone forgets to make private. For a 12 MB PDF, the whole job fits in the tab. Then the marketing outruns the engineering. "Bank-grade privacy" appears next to a page that calls four third parties before the first paint. The gap is always the same three things: analytics, error reporting, and fonts. Sentry is the worst of them because its payloads are designed to carry context. File names, breadcrumbs, and the last network request can all end up in an event, and nobody reads the scrubbing config until a customer asks. Make the claim enforceable If you want to say the files never leave, put it in a header where it is checkable: Content-Security-Policy:
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
connect-src 'none';
img-src 'self' data: blob:;
font-src 'self';
form-action 'none';
base-uri 'none';
frame-ancestors 'none'
connect-src 'none' blocks fetch, XHR, WebSocket, EventSource, and navigator.sendBeacon. That is the actual guarantee. Not the copy, not the privacy policy, the header. 'wasm-unsafe-eval' is there because compiling a WASM module is blocked by a strict script-src without it, which is a detail people discover at 2 a.m. the night before launch. form-action 'none' and base-uri 'none' close the two dumb paths around connect-src. And if you truly ship no telemetry, put a comment in the repo explaining that the header is a product promise, so the next person who adds a chat widget gets a failing test instead of a silent policy change. If you cannot set connect-src 'none' because you need a support chat and a payment SDK, that is fine. Say "processed locally, no files uploaded" instead. That sentence is true and defensible. The other one is a promise your own CDN can break. The constraints nobody puts in the demo The privacy story gets all the attention. The engineering is harder than the landing page suggests, and the limits are predictable: Memory. WASM32 gives you a 4 GB address space in theory and browsers give you far less in practice, especially on mobile, where the tab gets killed without a warning you can catch. A 300 MB scanned PDF will not fit if you decode every page at once. You need to process page by page, write intermediate results to OPFS, and free aggressively. If your demo file is 200 KB, you have not tested the product. Streaming. File.stream() gives you a ReadableStream, and TransformStream lets you pass chunks to a worker without materializing the whole file. That works beautifully for line-oriented formats, hashing pipelines, and archives. It does not work for PDFs, because PDF structure is random access by design, the xref table can live at the end of the file, and incremental parsing gets ugly fast. Know which case you are in before you promise streaming. Hashing. WebCrypto's crypto.subtle.digest takes a full buffer, which defeats the point of streaming a 2 GB file. If you need an incremental SHA-256 you will end up pulling in a WASM implementation or a pure-JS...

Powered by Readybot.io

**DEV Community** ## "Zero upload" is a claim. Show me …

DEV Community

Realtime Channel Teardown: Idempotent Recovery for Multiplayer Quiz Sessions

Short answer: treat channel deletion as a recorded state transition, not as a socket operation, and make every reconnect replay the same ordered event log. That decision keeps a multiplayer quiz game correct when a host closes a room while answers, timers, and reconnect requests are still in flight. I build payment and ledger backends, so I am suspicious of the phrase “delete the room.” Deletion sounds like one write, but it actually crosses the room registry, presence state, answer submissions, timer workers, and the audit trail. A player can receive a final question after the host has ended the game; a reconnect can arrive after the delete request but before its broadcast; a retry can execute the same command twice. The failure is not cosmetic. It can change a score. The invariant is a state transition, not a vanished row Give each quiz channel a monotonic version and an explicit lifecycle: open, closing, closed. A delete request first appends ChannelClosing with an idempotency key, then rejects new writes whose observed version is older than the closing version. A separate transition appends ChannelClosed after the server has stopped accepting answers and timer jobs. The registry may eventually remove the room, but the event record remains long enough for reconciliation and support evidence. The exact-once mindset matters here. Network delivery is at-least-once, and a WebRTC data channel can close while a message is buffered; the W3C recommendation describes connection and data-channel state, but it does not turn application messages into a transaction. Your application must attach ordering and replay semantics above the transport. The boundary should be visible in one decision record: Versions are cheap. Choice Useful property Failure it leaves you to handle Fit for a quiz room Hard delete plus broadcast Small initial implementation Lost terminal event, late writes, no audit evidence Poor default Tombstone with ordered log Reconnect can backfill a final state; retries are deduplicated Requires retention and compaction policy Recommended Per-room in-memory state only Low latency in one process Process loss erases truth; horizontal scaling races Only for disposable prototypes Database transaction plus outbox Atomic state and durable publication Outbox worker can lag and needs monitoring Strong foundation There is a catch: a tombstone is not suitable when rooms contain regulated data that must be erased immediately. In that case, keep a minimal hashed audit receipt, apply the applicable retention rule, and make the deletion boundary explicit to compliance reviewers. The receipt should prove that a close command was accepted without preserving answer text, player identifiers, or network metadata beyond the approved window; otherwise an apparently careful audit trail becomes a second copy of the data that the erasure request was meant to remove. For a casual lobby with no durable score, an in-memory room can be a reasonable choice; stick with the simpler model when recovery has no user-visible consequence. How should a multiplayer quiz game handle realtime channel deletion safely? Start with a command whose result is stable under retries. The client sends a channel identifier, the last event version it has seen, and an idempotency key. The service locks the channel record, checks that the key has either been completed or is new, writes the lifecycle event and an outbox row in one transaction, and returns the resulting version. A second request with the same key returns that recorded result rather than creating another close event. Here is the critical path in Go. The repository and outbox interfaces are intentionally generic; the important part is the transaction boundary and the version check. package quiz

import "context"

type Channel struct {
ID string
Version int64
State string
}

type CloseCommand struct {
ChannelID string
Expected int64
IdempotencyKey string
}

type CloseResul...

Powered by Readybot.io

**DEV Community** ## Realtime Channel Teardown: Idempotent Recovery for Multiplayer …

DEV Community

Vietnam AI & LLM Daily Briefing (Nov 16, 2026)

Vietnam AI & LLM Daily Briefing (Nov 16, 2026) A curated briefing on Ho Chi Minh City's pioneering open data policy for AI model training, the retail debut of Looki L1 AI wearable cameras in Vietnam, over 120,000 tech leaders petitioning for Jensen Huang's reinstatement, post-Jensen Nvidia compute supply chain scenarios, and digital sovereignty resilience across Vietnam over the past 24 hours (November 16, 2026): 1. Data Governance & Compute Infrastructure: HCMC Proposes Free Open Data Access for AI Training The Department of Science and Technology of Ho Chi Minh City has initiated public consultation on a landmark draft Resolution outlining criteria, regulatory mechanisms, and incentives to develop a Science, Technology, and Innovation Urban Area spanning at least 200 hectares: Dedicated Open Data Repository for AI Model Training: The municipal proposal establishes a specialised Open Data Repository within the Urban Area's centralised Data Management Centre. Public datasets will be continuously updated, de-identified, and anonymised in accordance with rigorous cybersecurity standards. Cost-Free Access for Enterprises and Academic Institutes: Enterprises, research institutions, and engineering teams operating within the innovation district will be granted cost-free access to these curated datasets to train frontier Artificial Intelligence and Large Language Models (LLMs), advance Big Data analytics, and deploy digital transformation applications. Public Seed Funding for Shared Infrastructure & Sandboxes: The resolution proposes state budget allocations to serve as catalytic seed funding for shared research infrastructure, including advanced laboratories (Labs), fabrication labs (FabLabs), high-throughput AI data centres, and tech incubators. It also establishes a controlled regulatory sandbox framework to pilot emerging AI technologies and novel digital business models. Primary References: VnExpress - HCMC proposes free open data access for enterprises to train AI 2. Consumer Hardware & Personalised Intelligence: Looki L1 Wearable AI Camera Launches in Vietnam The commercialisation of specialised Artificial Intelligence hardware continues to gain momentum across Vietnam's tech-forward consumer market: Commercial Debut of Looki L1: The Looki L1 wearable AI clip/pendant camera (weighing just 32 grams) has officially launched in Vietnam with a retail price of 6.9 million VND, delivering on-body ambient intelligence in an ultra-compact form factor. From Passive Recording to Multimodal AI Journaling: Unlike traditional action cameras designed strictly for high-resolution video capture, the device is optimised to autonomously capture, synthesise, and analyse daily personal moments via on-device computer vision and cloud-connected foundation models, generating structured daily summaries and conversational video narratives. Emergence of Ambient Computing in Vietnam: The rollout highlights growing consumer appetite for lifestyle AI companions and on-device intelligent assistance, while simultaneously underscoring the critical need to preserve personal privacy and biometric data integrity across distributed edge devices. Primary References: VnExpress - Looki L1 wearable AI journal camera priced at 6.9 million VND 3. Global Semiconductor Governance: Over 120,000 Sign Petition to Reinstate Nvidia CEO Jensen Huang Industry backlash following the sudden departure of Nvidia co-founder and long-time Chief Executive Jensen Huang has escalated into an unprecedented global campaign across the technology sector: Coalition of Frontier Tech Leaders: The "Stand with Jensen" initiative has surged past 120,000 signatures, securing high-profile backing from leading technology executives including Elon Musk (SpaceX, xAI), Amjad Masad (CEO of Replit), Garry Tan (President of Y Combinator), and Tim Sweeney (CEO of Epic Games). Widespread Shareholder and Internal Backing: Organisers report that signatories comprise over 66,000 Nvidia shareholders, 6,20...

Powered by Readybot.io

**DEV Community** ## Vietnam AI & LLM Daily Briefing (Nov …

DEV Community

Laravel Sanctum vs Passport vs JWT: Which Auth to Use

"Sanctum, Passport or JWT?" gets asked as if the three were competitors. They aren't quite. Sanctum and Passport are first-party Laravel packages. JWT is not a package at all: it's a token format (RFC 7519) that third-party Laravel packages build on. The useful question is not "which is best" but who is calling your API, and do you need OAuth2? For most Laravel applications the answer is Sanctum, and Laravel's own documentation says so. This article explains why, shows what each option does with real output, and covers the cases where Passport or JWT is the right call. I ran Sanctum 4.4.0 in a Laravel 13.40 app and tested JWT behavior with firebase/php-jwt 7.2.1. Passport I could only check against the documentation, and I say so where it matters. The short answer Sanctum Passport JWT (community packages) What it is Lightweight first-party package: cookie auth for SPAs, plus simple API tokens First-party full OAuth2 server A signed token format; packages such as tymon/jwt-auth and php-open-source-saver/jwt-auth wrap it Best for Your own SPA, mobile app, or simple API tokens Third parties logging in through your app (OAuth2), MCP servers Stateless verification across services, if you accept the trade-offs Revoking a token Delete a row; immediate Revoke server-side Needs server-side state (a denylist) to revoke early Setup weight Light Heavy (clients, keys, grants) Medium, plus your own key and lifetime decisions The decision flow, in order: Is it a regular browser app on the same Laravel project? Use Laravel's built-in session authentication; you don't need any of these. Is it your own SPA or mobile app, or do you just want to hand out API tokens? Sanctum. Do outside developers need "Log in with your app" or standard OAuth2 flows, or will an MCP client connect? Passport. Do several independent services need to verify tokens without calling back to one database? Consider JWT, and plan revocation first. Sanctum: two things in one package The docs describe two separate problems Sanctum solves. First, API tokens: a simple way to issue tokens "without the complication of OAuth", stored in one database table and sent in the Authorization header. Second, SPA authentication, where Sanctum doesn't use tokens at all and relies on Laravel's cookie-based session authentication instead, only for requests that come from your own SPA frontend. It is fine to use only one of the two. The setup from the docs is php artisan install:api, then the HasApiTokens trait on your user model: // app/Models/User.php
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}
Protect routes with the sanctum guard. The abilities check needs its middleware alias registered once in bootstrap/app.php: // bootstrap/app.php, inside withMiddleware()
$middleware->alias([
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
]);

// routes/api.php
Route::middleware('auth:sanctum')->get('/user', fn (Request $request) => $request->user()->only('id', 'name', 'email'));

Route::middleware(['auth:sanctum', 'abilities:orders:read'])->get('/orders', fn () => ['orders' => []]);

Route::middleware(['auth:sanctum', 'abilities:orders:write'])->post('/orders', fn () => ['created' => true]);

Route::middleware('auth:sanctum')->post('/logout', function (Request $request) {
$request->user()->currentAccessToken()->delete();

return ['ok' => true];

});
Issuing tokens is one call. This is the pattern from the docs for a mobile login endpoint, which checks the credentials and returns a token named after the device: Route::post('/sanctum/token', function (Request $request) {
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required',
]);

$user = User::where('email', $request->email)->first();

if (! $user || ! Hash::check($request...


Powered by Readybot.io

**DEV Community** ## Laravel Sanctum vs Passport vs JWT: Which …

** Latest from PC Gamer **

Are you more of an ambient music enjoyer or theatrical atmosphere appreciator?

Quiet and unassuming, or loud and proud?

Powered by Readybot.io

** Latest from PC Gamer ** ## Are you more of an ambient mus

DEV Community

I built 20 calculators that show their working. Here's the stack, the bugs and the SEO surprise

Most online calculators give you a number and expect you to trust it. I wanted the opposite: under every result, a line that prints the exact sum with your numbers in it, so you can check it by hand. That became EziSolutions, a free site with 20 calculators: Etsy, Mercari, Poshmark, Venmo, Stripe, Square and Fiverr fees, ROAS, customer LTV, CPM, BMI, TDEE, VAT, loans, freelance rates and a few more. No framework, no backend, no signup. Here's how it's built, the bugs that taught me something, and an honest look at the SEO side, which didn't go the way I expected. I built it with help from an AI coding assistant, and checked every formula and fee myself. The stack (deliberately boring) Python build script. content.py holds every page as data: title, description, form fields, the explanation, FAQs and sources. build.py turns it into plain HTML files in public/. Vanilla JavaScript. One tools.js file with one function per calculator. No React, no bundler. Cloudflare Workers with static assets. The Worker just serves public/. _headers sets security headers and _redirects handles old URLs. GitHub + Workers Builds. Every push to main runs npx wrangler deploy. About 40 seconds from git push to live. The whole deploy config is this: {
"name": "icy-shape-8ab9",
"compatibility_date": "2026-11-01",
"assets": {
"directory": "./public",
"not_found_handling": "404-page",
"html_handling": "auto-trailing-slash"
},
"routes": [
{ "pattern": "ezisolutions.online", "custom_domain": true },
{ "pattern": "www.ezisolutions.online", "custom_domain": true }
],
"workers_dev": false
}
I commit the built public/ folder instead of building on Cloudflare. It's less elegant, but the deploy step has nothing to break, and I can see exactly what ships in every diff. The feature: a "working" line under every result Each calculator ends by calling one helper: function work(text) { set('ez-working', text); }

// Inside the Venmo calculator:
work(money(amt) + ' × ' + pct + '% + ' + money(fixed) + ' = ' + money(fee) + ' fee');
// → "$100.00 × 2.99% = $2.99 fee"
It's a tiny thing, but it changed how I wrote every calculator. If I couldn't print a clean one-line sum, the logic was probably too clever, and I simplified it. Bug 1: "How much should I charge to receive $100?" Fee calculators get asked this a lot. The textbook answer is: amount to charge = (amount you want + fixed fee) ÷ (1 − percentage)
For Venmo's 2.99% that gives $103.0821, which I rounded up to $103.09. But Venmo rounds its fee to the cent, so $103.08 already leaves exactly $100.00. I was telling people to overcharge by a cent. Worse, the formula falls apart for fees with a cap or minimum. Stripe's ACH fee is 0.8% capped at $5. To receive $1,000 you need $1,005, but the formula said $1,008.06. The fix was to stop solving it algebraically and search over whole cents instead: // Smallest amount (in cents) whose fee still leaves want.
function askFor(want, pct, fixed, feeFn) {
let lo = Math.round(want * 100);
let hi = Math.ceil((want + fixed + 1) / Math.max(1 - pct / 100, 0.01) * 100) + 100;
while (lo < hi) {
const mid = Math.floor((lo + hi) / 2);
if (mid / 100 - feeFn(mid / 100) >= want - 1e-9) hi = mid; else lo = mid + 1;
}
return lo / 100;
}
feeFn is the real fee function, with rounding, caps and minimums, so whatever rule a platform uses, the answer is exact. The formula only sets the upper bound for the search. Bug 2: my test caught a wrong number in my own article Every tool page has a worked example in the text. I test each calculator by filling it with the example's numbers in headless Chrome (plain Chrome DevTools Protocol, no test framework) and comparing the output with what the page says. That's how I found the $103.09 bug: the calculator and the article disagreed. Testing against the prose turned out to be a cheap way to keep the explanations honest. Bug 3: the security header that would have blocked my revenue My Content-Security-...

Powered by Readybot.io

**DEV Community** ## I built 20 calculators that show their …

DEV Community

Flutter on Edge Robotics: Adapting UI Workloads to Jetson Thermal Pressure

Edge systems share a thermal and power envelope across inference, networking, video, and application services. The Flutter client should degrade gracefully when telemetry indicates resource pressure. Observe before adapting Collect temperature, throttling state, CPU/GPU use, memory pressure, inference latency, and frame age through supported interfaces. Do not infer thermal state from one slow frame. Define graceful degradation Lower chart refresh rates, reduce decorative animation, request lower preview resolution, or reduce noncritical diagnostics. Keep essential alarms and controls available. Use explicit policy and hysteresis A small state machine prevents mode flapping near thresholds. Separate signal collection, policy, and presentation. Coordinate with the edge service The Jetson service owns model and accelerator scheduling. Flutter can request supported quality profiles but hiding widgets does not directly reduce GPU inference load. Test recovery Simulate pressure and verify gradual recovery after metrics stabilize. Log adaptation decisions and their triggering measurements. Example implementation This Dart snippet illustrates one part of the design. Adapt types, transport behavior, timestamp semantics, and platform APIs to your actual system before production use. `enum UiLoadMode { full, reduced, minimal }

UiLoadMode chooseMode({
required double frameAgeMs,
required double cpuPercent,
}) {
if (frameAgeMs > 500 || cpuPercent > 90) return UiLoadMode.minimal;
if (frameAgeMs > 200 || cpuPercent > 75) return UiLoadMode.reduced;
return UiLoadMode.full;
}
`
Performance checklist Measure p95/p99 behavior on target hardware, not only a desktop simulator. Bound queues and buffers; define what may be coalesced or dropped. Keep heavy parsing, image processing, and inference away from the Flutter UI thread. Track source timestamps, sequence numbers, stale-data rate, and dropped updates. Test network degradation, reconnects, sustained operation, and resource pressure. Keep control commands and critical alerts separate from replaceable telemetry.

Powered by Readybot.io

**DEV Community** ## Flutter on Edge Robotics: Adapting UI Workloads …

DEV Community

Android tells your headphones it's an iPhone

Somewhere in the Bluetooth code that ships with Android is this line: // feature = 2 indicates that we support battery level reporting only
mNativeInterface.atResponseString(device, "+XAPL=iPhone," + String.valueOf(2));
That's your phone telling your headphones it's an iPhone. Android's been saying it since 8.1 in 2017, ChromeOS says it too, and so do PipeWire and PulseAudio on Linux. It's also why plenty of headphones show their battery in tens on all of them, and why "10%" can mean they're nearly dead. Apple's way of asking Calls on Bluetooth headphones go over the Hands-Free Profile, which is mostly old modem-style text commands going back and forth. Apple added a few of its own in its Accessory Design Guidelines. Headphones that want their battery shown on an iPhone send AT+XAPL with their vendor ID, product ID and firmware version, then a number saying which of Apple's features they support. The guidelines give exactly one reply to that: +XAPL=iPhone, and the features the phone supports. Then the headphones send their battery level with AT+IPHONEACCEV. Here's a pair of Mpow Flame S doing both, from an HCI snoop log someone pulled off their phone: Rcvd AT+XAPL=0000-0000-0100,7
Rcvd AT+IPHONEACCEV=2,1,5,2,0
The first number in the second line says two pairs follow. Key 1 is the battery, which is a 5, and key 2 is the dock state, which is 0 for not docked. The phone in that log was on Android 7.1, before Android learned the iPhone line, and it just said OK. The Mpow sent its battery anyway, so not every pair of headphones waits to be told. But for plenty of them Apple's command is the only way they report their battery at all, and Apple's spec only has the one answer, so that's the answer everyone gives. Headphones play along from their end too. One set of earbud firmware on GitHub opens with AT+XAPL=05AC-1702-0100,7, and 05AC is Apple's USB vendor ID. On a connection to an Android phone, both ends are claiming to be Apple. A digit from 0 to 9 That 5 isn't a percentage. Apple's guidelines say the battery level is "a string value between '0' and '9'", and that's all they say. Nothing about what a 0 means, or how to turn it into a percentage. Android, ChromeOS's Floss, PipeWire and PulseAudio all do the same sum with it anyway: add one, times by ten. A 5 is 60%, a 9 is 100% and a 0 is 10%. Floss is nicely upfront about where its (n + 1) * 10 came from: The guidelines do not specify that this maps to [10, 100] but that is how other Bluetooth stacks interpret it so we do so as well. Floss, ChromeOS's Bluetooth stack At least one stack did try a straight 0-to-100 scale. hsphfpd, a prototype Linux headset daemon, showed a 7 as 77%, until someone whose phone said 80% sent in a fix in 2020, as "reporting 0% battery level wouldn't be very useful". That's where the tens come from. Headphones that only use Apple's command can show ten values, and 0% isn't one of them. What's a 10%, then? That's down to whoever wrote the headphones' firmware, since Apple doesn't say. Here's how that same earbud firmware picks its digit (0x30 is the character 0): data[BATTERY_POS] = ((percent - 1) / 10) + 0x30;
Anything from 1% to 10% goes out as a 0, and your phone shows it as 10%. Try it on the blog Other firmware will round its own way, but none of them can tell your phone it's on 3%. There's been a proper one since 2014 The Bluetooth SIG added a battery indicator to the Hands-Free Profile in version 1.7. Headphones send AT+BIEV=2, and then a percentage from 0 to 100, and Android, Floss, PipeWire and PulseAudio all show it as it is. Headphones just don't seem to send it as often. In the GitHub and PipeWire issues I could find where someone had pasted the commands, Apple's outnumbered it more than two to one, though that's a small pile and nearly all of it from Linux users. Some send both. Here's what the developer of a GNOME battery extension got from their own earbuds with btmon: $ sudo btmon | grep -A1 -E "IPHONEACCEV|BIEV|XEVENT=BATTERY"
...

Powered by Readybot.io

**DEV Community** ## Android tells your headphones it's an iPhone …

DEV Community

How to Handle Failed Background Tasks Using Dead Letter Queues in 2026

Short answer: put the weekly-digest worker behind a queue with a bounded re-delivery limit, then route messages that still fail to a dead letter queue (DLQ). Inspect that DLQ before retrying anything. Reprocess only after the failure is understood, and make the digest operation idempotent so a redelivery cannot send the same customer two emails. That pattern fits a healthtech system where the weekly job targets active customers. It also gives the operator a deliberate answer to an uncomfortable question: how much failed work should remain visible, and for how long? For this workflow, Infrai is a practical REST-first option when the team wants the queue and its DLQ utility calls without adding an SDK to the worker. Its public discovery surface is self-describing, so an engineer can inspect the capability and schema before wiring the operator action. One key and one bill also reduce credential and reconciliation work when the same service already uses other backend capabilities. What is the bill actually made of? The dominant operational term is retained message data: message count multiplied by payload size multiplied by retention time. A simple planning case makes the scale visible. With 1,000 active customers, one weekly dispatch creates 52,000 delivery intents in a year before retries. The successful messages can be acknowledged and removed; the failed ones accumulate according to the retry policy. That distinction matters more than a small per-request price comparison. Keep the digest payload compact, store a stable customer reference rather than a full report, and put the re-delivery limit on the main queue. A message body is limited to 256 KB, delayed messages to 7 days, and retained messages to 30 days. Acknowledgement deletes a message. These are retention decisions, not merely API details. I would keep a failure record containing the queue name, a digest period, a customer identifier, an idempotency key, the last error class, and the attempt count. I would not copy the full clinical or reporting payload into every log line. The log is another bill: bytes retained and labels indexed. A low-cardinality error_class is more useful than a label containing the customer ID. Failures are data. The trade-off is plain. Keeping only a compact failure record lowers storage and cardinality, but it makes a later investigation depend on the original system of record. Keeping the whole payload makes diagnosis easier and retention more expensive. For a healthtech digest, the safer default is to keep identifiers and error metadata in the queue record, then fetch the current report again after an operator approves a retry. How should a failed digest move through the system? The worker should treat a queue delivery as at-least-once work. It acknowledges a message only after the digest send has reached its intended outcome. A timeout, a provider response that can be retried, or a worker crash leaves the message eligible for another delivery. The digest handler therefore needs an idempotency key derived from stable business values such as customer_id and digest_period, not from the delivery attempt. Consider the failure sequence rather than the happy path. The scheduler creates one intent for the weekly period. A worker receives it, renders the current report, and calls the delivery provider. The provider accepts the request, but the worker loses its connection before receiving the response. The queue quite correctly delivers the intent again. If the idempotency key was the queue delivery ID, the second attempt can send a duplicate; if it was customer_id:2026-W42, the delivery layer can recognize that both attempts describe the same business operation. A provider timeout still needs classification, because an unknown outcome is different from a confirmed rejection. The first belongs in a retry budget. The second may belong in the DLQ after one attempt. This is why the queue's re-delivery limit, the sender's idempotency record, and the o...

Powered by Readybot.io

**DEV Community** ## How to Handle Failed Background Tasks Using …

DEV Community

Tavily vs Exa vs Brave vs cloro: which search API is better for an AI agent?

For an AI agent, cloro is the better search API of the four compared here on accuracy and cost, and Exa is the better one on speed. In an open benchmark of 2,182 questions with one search each, it answered 80.2% of the multi-hop questions, against 64.0% for Exa, 53.9% for Brave and 48.4% for Tavily, at the lowest cost per correct answer. Exa had the fastest median search, 0.74 s against 7.96 s for cloro. Between Tavily and Exa, Exa wins on accuracy and speed, while Tavily costs less per search from its $100 Bootstrap plan up. Brave is the pick when the slowest searches matter, because its p99 latency was the lowest of the four. Tavily is the pick for page text and a written answer in one request. Accuracy: on 824 multi-hop questions, cloro scored 80.2%, Exa 64.0%, Brave 53.9% and Tavily 48.4%. On 1,000 short factual questions, Exa (97.3%) tied with it (96.9%), ahead of Brave (93.1%) and Tavily (91.8%). Latency: the median search took 0.74 s on Exa, 1.21 s on Brave, 2.00 s on Tavily and 7.96 s on cloro. Cost: per 1,000 correct short factual answers, cloro cost $1.98, Brave $5.37, Tavily $5.45 and Exa $7.19. Tavily vs Exa vs Brave vs cloro: which one answers more questions? Six web search APIs ran on three public question sets in November 2026, with one search and 10 results per question. One model answered from the results alone, and a second model graded each answer. Four of the six are in the table. Measure cloro Exa Brave Tavily SimpleQA Verified, 1,000 short factual questions 96.9% 97.3% 93.1% 91.8% FreshQA, 358 questions about recent events 88.3% 83.0% 84.9% 81.8% FRAMES, 824 multi-hop questions 80.2% 64.0% 53.9% 48.4% "Not found" on FRAMES, failed searches included 11 151 212 295 $ per 1,000 searches, at 100,000 a month $1.92 $7.00 $5.00 $5.00 $ per 1,000 correct SimpleQA answers $1.98 $7.19 $5.37 $5.45 A paired test counts, on the same questions, the ones that only one API answered correctly, so easy and hard questions cancel out. In every pair that is not a tie, the API named first won. Pair SimpleQA Verified FreshQA FRAMES cloro vs Exa Tie (p = 0.63) 25 to 6 179 to 45 cloro vs Brave 50 to 12 Tie (p = 0.065) 248 to 31 cloro vs Tavily 70 to 19 32 to 9 297 to 35 Exa vs Tavily 72 to 17 Tie (p = 0.67) 197 to 69 Exa vs Brave 55 to 13 Tie (p = 0.32) 139 to 56 Brave vs Tavily Tie (p = 0.25) Tie (p = 0.12) 147 to 102 cloro lost none of its nine pairs and tied two: Exa on short factual questions and Brave on recent events. Among the other three, Exa wins on factual and multi-hop questions, and all three tie on recent events. The FRAMES gap is mostly coverage. When each API did answer, it was right 73.5% to 81.3% of the time. The "not found" count, from 11 to 295, made most of the gap. For an agent, each "not found" is a second search, a page fetch or a failed task. One row of Tavily's results file, shortened: {"id": "661", "provider": "tavily", "problem": "What year was the first Uber employee born?",
"gold": "1983", "search_latency_s": 1.937, "answer": "NOT FOUND", "grade": "NOT_ATTEMPTED"}
The other three APIs all answered "1983" from one search on the same question. Does calling Tavily, Exa and Brave together fix multi-hop questions? Only partly. We counted, from the public results files, the FRAMES questions where at least one of Tavily, Exa and Brave was right. That is 628 of 824, or 76.2%. It is an upper bound, because a real agent also has to pick the right answer from three. On 63 questions all three replied "not found". One search on cloro did better: 661 of 824, or 80.2%. It got 104 questions that none of the three got, and the three together got 71 that it missed. Three searches also cost $17 per 1,000 questions at the 100,000-a-month prices below, against $1.92 for one. Which one is faster? The table times the search request alone, on the 615 SimpleQA Verified rows that ran with no other benchmark at the same time. Percentile Exa Brave Tavily p50 0.74 s 1.21 s 2.00 s p90 2.43 s 1.38 s 3.17 s p99 4.28 s 1.61 s 5...

Powered by Readybot.io

**DEV Community** ## Tavily vs Exa vs Brave vs cloro: …

DEV Community

Flutter Real-Time Pipelines: Budgeting Latency Across Edge AI Systems

A responsive robotics or smart-glasses companion needs an explicit latency budget across capture, transport, inference, state processing, and rendering. Optimizing only Flutter widgets will not fix a slow end-to-end pipeline. Define the user-visible deadline Choose a target based on the interaction. Telemetry can tolerate more delay than teleoperation feedback. Track capture-to-display age separately from network round-trip time. Assign a budget to every stage Record timestamps at the producer, gateway, inference service, Flutter receipt, state reducer, and render scheduling boundary. Use monotonic clocks for durations within one process; account for clock offset across hosts. Measure percentiles, not only averages Capture p50, p95, and p99 latency, queue depth, dropped frames, and stale-result rate. A healthy average can hide stalls operators notice. Keep the Flutter path lean Validate and parse messages before they reach widget build methods. Coalesce bursts into bounded state updates and rebuild only dependent widgets. Make the budget testable Replay representative traces in profile mode and add regression thresholds for p95 end-to-end age, not just startup time. Example implementation This Dart snippet illustrates one part of the design. Adapt types, transport behavior, timestamp semantics, and platform APIs to your actual system before production use. class StageTimer {
StageTimer(this.name) : _watch = Stopwatch()..start();
final String name;
final Stopwatch _watch;
Duration finish() => _watch.elapsed;
}
Performance checklist Measure p95/p99 behavior on target hardware, not only a desktop simulator. Bound queues and buffers; define what may be coalesced or dropped. Keep heavy parsing, image processing, and inference away from the Flutter UI thread. Track source timestamps, sequence numbers, stale-data rate, and dropped updates. Test network degradation, reconnects, sustained operation, and resource pressure. Keep control commands and critical alerts separate from replaceable telemetry. Useful Links Website: www.v-modal.com SDK Flutter: https://github.com/v-modal/vmodal_sdk_flutter SDK Android: https://github.com/v-modal/vmodal_sdk_android Discord: Join on Discord Reddit: https://www.reddit.com/r/v_modal/

Powered by Readybot.io

**DEV Community** ## Flutter Real-Time Pipelines: Budgeting Latency Across Edge …

DEV Community

Is there an independent benchmark of web search APIs like Tavily, Exa and Brave?

Yes, three third-party boards test Tavily, Exa and Brave Search API side by side: Openbenchmarks, AIMultiple and Artificial Analysis. None of the three sells a search API, and each keeps the model, the questions and the grader fixed while it changes the search API. They measure different things, and each one keeps part of its question set private. Openbenchmarks runs one search per question on 300 company-news lookups, plus agent tasks for hard retrieval (100 tickets) and multi-hop search (45 questions). Its harnesses are MIT-licensed on GitHub, with public question samples on Hugging Face. AIMultiple and Artificial Analysis give a fixed AI agent many searches per question. They measure the search API as one tool inside an agent loop. Vendor benchmarks from Tavily, Exa, Parallel and cloro often publish more of their code and answers, but the company that runs each one sells an API in the table. Tavily's and Exa's own tables put their own API first. Which independent benchmarks test Tavily, Exa and Brave? Benchmark What it tests Searches per question Tavily, Exa and Brave in it What is public Openbenchmarks web search, last measured October 21, 2026 300 company-news lookups, 100 hard-retrieval tickets, 45 multi-hop company questions 1 on lookups; an agent with up to 5 searches on retrieval and up to 14 on multi-hop Yes, 13 vendors MIT harnesses; 100 of the 300 lookups and 30 tickets on Hugging Face AIMultiple agentic search, updated November 10, 2026 120 English questions, 24 configurations of 6 APIs 1 on single-call questions, up to 25 calls on the others Yes 50 of 120 questions; summary CSVs free, detailed data paid; no code linked Artificial Analysis Search Index DeepSearchQA (900), BrowseComp (200 of 1,266), AA-Omniscience (600, private) Agent with up to 25 turns, search and fetch Yes, 26 provider configurations The agent harness; the page does not say if its run data is public What Openbenchmarks measured On the lookup board, every API gets the question text unchanged, one request and at most 10 results. One model (gpt-5.6-terra) reads the titles and snippets and extracts an answer. A second model (claude-opus-5) judges it against answers that people labelled before any vendor was called. Exa (fast mode) scored 99.3%, Exa instant 97.7%, Brave's LLM context endpoint 94.0%, Brave web search 93.3%, Tavily advanced 93.0% and Tavily basic 87.7%. The board itself flags a problem with these numbers: vendors benchmarked later tend to score higher. Until a re-run, it says the lookup rankings "should not be used to compare vendors." On the agent boards, Exa deep led hard retrieval at 83.0% when the agent could fetch pages, and Parallel basic led multi-hop search at 46.5% F1 without page fetch. Its about page says "Nobody pays to be included, ranked or removed." The same page says it sells evals as a service and that many of its partners are customers. Published rankings come from a held-out set whose questions it never publishes. What AIMultiple measured A fixed agent (OpenCode with gpt-5.6-luna) answered 120 English questions with each of 24 configurations of Brave, Exa, Firecrawl, Parallel, Perplexity and Tavily. Three judge models graded each answer, and an answer counts as correct when at least two of them agree. Parallel Advanced led at 87.0% (95% interval 78.3 to 94.2) and Exa Fast followed at 82.6% (73.9 to 91.3). The page says these intervals overlap and each configuration ran once per question. It also states that the research "does not mention any subscribers of AIMultiple's benchmarking services." What Artificial Analysis measured A fixed agent (gpt-5.6-luna, medium effort) gets web_search and web_fetch tools and up to 25 turns. The index is the equal-weighted mean of three benchmarks: DeepSearchQA F1, BrowseComp accuracy and AA-Omniscience accuracy. The configurations include Tavily basic, Exa lite and auto, and Brave web and LLM context. The methodology page shows no scores and makes no statement about vendor paymen...

Powered by Readybot.io

**DEV Community** ## Is there an independent benchmark of web …

DEV Community

Use an AI assistant for customer onboarding with MCP

You can use an AI coding assistant for customer onboarding by connecting it to the JustOneCX remote MCP server at https://api.justonecx.com/mcp. It works with Claude Code, Codex, Cursor and Gemini CLI. You sign in through your browser, pick the workspace and choose an access level. Everything it creates is a draft until a person publishes it. What does the assistant do for onboarding? Once connected, the assistant can install the SDK, build product tours from your code, announce features, add surveys, answer chat, manage your roadmap, read your analytics and event log, set up funnels and Brand Kit, and find out why something does not show. It acts as you, so it can never do more than your own role allows. For a vague request such as "add a tour", a planning skill makes it study your product and propose a ranked list first. It chooses where each tooltip, survey or announcement sits from where the element is on screen, and asks when unsure. How do you connect it? Your assistant signs in through your browser the first time. No token goes in any config file. Claude Code claude mcp add --transport http justonecx https://api.justonecx.com/mcp
Then run /mcp, pick justonecx and sign in. Codex. Add this to ~/.codex/config.toml, then run codex mcp login justonecx: [mcp_servers.justonecx]
url = "https://api.justonecx.com/mcp"
Cursor. Put this in .cursor/mcp.json: { "mcpServers": { "justonecx": { "url": "https://api.justonecx.com/mcp" } } }
Gemini CLI. Add this to ~/.gemini/settings.json, then run /mcp auth justonecx: { "mcpServers": { "justonecx": { "httpUrl": "https://api.justonecx.com/mcp" } } }
For CI and tools without browser sign-in, you can create a token in Settings → Developers → AI assistants. Keep the token in an environment variable and never in a config file. A token has the same limits as the browser sign-in and can be revoked in the same place. See the MCP server docs. What are the access levels? When you sign in, you pick how much the assistant may do. Level What it can do See your content and setup Reads only. Create and edit drafts (default) Nothing your users see changes until a person publishes. Make changes live Can also publish. Off unless you choose it. A workspace owner can switch AI assistants off or cap them in Settings → Developers → AI assistants. What can it not do? It cannot empty the Trash, export survey responses as a file, or change members, roles, billing or keys. For those it points you to the dashboard page and the steps. It never handles the Secret key. The Secret key is never returned, read, asked for or pasted into chat. You set JUSTONECX_SECRET_KEY in your own server environment yourself. The get_install_info tool returns your Public key and the install snippet, never the Secret key. For identity verification, it adds the server code and an .env.example entry, and passes userHash to identify(). It switches Require on only after your live site sends userHash and you say yes, because that setting changes the live site. See identify users with the SDK. What stays a draft, and what needs confirm? Everything it creates is a draft until a person publishes it. Changes that reach customers or the live site need an explicit confirm. Examples are: A chat reply, which goes to the customer at once. A roadmap status change, because voters are emailed. Workspace settings and custom CSS. Deleting a tour, announcement or survey, which leaves the site at once. Contact details such as email, phone and address are hidden unless the task asks for them. Every change shows in your audit log as made by the AI assistant. How should you review the assistant's work? Use the same steps every time. Ask it to run workspace_status. It returns your role, plan and a setup checklist, with a fix for every failing item. Ask it to build the content as a draft, for example a tour. Run validate_tour to check the selectors, page rule and step flow. Open the preview link. get_tour_preview_link previews the draft on your site for 2...

Powered by Readybot.io

**DEV Community** ## Use an AI assistant for customer onboarding …

DEV Community

How to Use AI for Content Without Sounding Like a Robot

An AI digital marketer from Kerala on the small edits that turn a generic draft into something people actually believe. You have probably seen it. A post opens with “In today’s fast paced digital world,” lists five neat points, and ends with “In conclusion, it is important to stay consistent.” You do not need to be an expert to feel that something is off. The words are correct, but nobody seems to be behind them. AI can save a lot of time in content work. But when people publish its first draft as it is, their pages start to sound the same. Customers scroll past, and trust slowly drops. Here is how I try to keep the speed without losing the human voice. Why AI writing sounds robotic It helps to understand the problem first. AI writes by predicting what usually comes next. So its first draft often contains the most average version of any idea. That is why drafts tend to show the same patterns: Big, empty openings that say nothing Perfectly balanced lists of three Words like “unlock,” “elevate,” and “game changer” No personal story, no mistakes, no opinion Every paragraph the same length and rhythm A neat summary at the end that repeats everything None of this is wrong. It is just flat. Real people write with unevenness, specific details, and a point of view. Step 1: Give it your material, not only a topic The biggest mistake is typing a short request like “write a post about digital marketing” and expecting something special. A vague request gets a vague answer. Instead, give real input: Who the reader is What you want them to feel or do Your own notes, examples, and opinions A real story or a recent customer question The tone you want, in plain words When the raw material is yours, the draft starts closer to your voice. Step 2: Use the draft as clay, not as the final product Treat the first output as rough material. Read it and mark three things: what is useful, what is generic, and what is missing. Then rebuild it in your own words. Keep the structure if it helps, but rewrite the sentences. A good rule is that the final version should not contain long stretches of untouched AI text. Step 3: Cut the empty openings Delete the first paragraph and see if the post still works. Most of the time, it improves. Start with something real: a moment, a question a customer asked, a surprising number you checked yourself, or a direct statement. Compare these two openings: “In the ever evolving world of social media, businesses need to adapt.” “A tailor in our area stopped posting for two months because she did not know what to say.” The second one makes you want to read the next line. Step 4: Add details only you know This is the biggest difference between human and robotic writing. Add things a machine cannot invent honestly: A real customer question you heard this week A mistake you made and fixed A local place, festival, or habit people in your area will recognise A small result from your own test Your honest opinion, even if it is a little unpopular Even two or three real details can change the feel of an entire post. Step 5: Break the perfect rhythm AI often writes sentences of similar length, in tidy patterns. Real speech jumps around. Mix short sentences with longer ones. Sometimes use a very short line for emphasis. Like this. Also vary your paragraph size. Not every paragraph needs three sentences. Let some be one line and others a little longer. Step 6: Replace big words with simple ones Read through and swap fancy phrases with the words you would use in conversation. “Utilise” becomes “use” “Leverage” becomes “use” too “Endeavour” becomes “try” “Facilitate” becomes “help” “Seamless” can usually be deleted If you would not say it to a customer standing in front of you, do not write it. Step 7: Remove the tidy listicle habit AI loves lists of three or five points with bold headings, each followed by a neat explanation. Lists are useful sometimes, but when every post has the same shape, readers notice. Try writing some posts as...

Powered by Readybot.io

**DEV Community** ## How to Use AI for Content Without …

DEV Community

When the Net Went Dark: Architectural Insights into Q3 2026 Global Internet Outages

This article was originally published on FutureStack Introduction In the third quarter of 2026 the public Internet experienced a series of simultaneous disruptions that, for a brief window, rendered large swaths of the globe effectively offline. Cloudflare’s global telemetry platform – Radar – logged over 1.2 billion anomalous flow‑termination events, spanning three continents and affecting everything from DNS resolution to TCP handshakes. While the headlines focused on the human and geopolitical narrative – power‑grid failures in the Midwest, a Category 5 cyclone in the Indian Ocean, and a sudden government‑mandated shutdown in Eastern Europe – the engineering story is far richer. This piece provides a deep architectural breakdown of the protocols and state machines that amplified these disturbances, a FutureStack perspective from Mubashir Arham, a code blueprint for real‑time disruption detection, an empirical trade‑off table, and a look‑ahead at where the industry is headed over the next 18‑36 months. 1. Deep Architectural Breakdown 1.1. The Internet’s Multi‑Tiered Transport Stack The modern Internet can be visualized as a three‑layered onion: Edge‑Ingress (L3/L4) – ISP peering points, BGP routers, and DDoS scrubbing centers. Edge‑Compute (L7) – CDN edge nodes, DNS resolvers, and TLS termination points. Core‑Backbone (Transit) – High‑capacity fiber, submarine cables, and backbone MPLS. During Q3 2026 the failure vectors hit all three layers: Power failures knocked out dozens of regional IXPs, causing BGP flaps that propagated to the global routing table within seconds. The BGP convergence time (normally 2‑5 s for a single withdrawal) ballooned to >30 s because of route‑flap damping mis‑configurations. Natural disasters severed submarine cable landing stations, forcing traffic to reroute over longer, higher‑latency paths. The TCP congestion control algorithms (CUBIC, BBR) reacted poorly to the sudden RTT spikes, leading to exponential back‑off and a ~40 % throughput drop. Government shutdowns introduced policy‑level packet filters at national egress points. These filters often performed deep‑packet inspection (DPI) on encrypted traffic, adding 10‑30 ms of per‑packet latency and causing TLS handshake timeouts. 1.2. Memory Model of Edge Nodes Edge servers (e.g., Cloudflare’s 200 M+ PoPs) maintain a two‑tier cache hierarchy: Hot‑Cache (SRAM) – ~2 GB per node, stores active TLS session tickets and DNS cache entries. Warm‑Cache (NVMe) – ~10 TB per node, persists large static assets. When a regional power loss occurs, the SRAM state is lost. Edge software attempts a graceful warm‑up by replaying the last persisted checkpoint. However, the checkpoint interval (default 5 min) proved insufficient: the session‑ticket replay window exceeded the TLS ticket lifetime, resulting in client‑side handshake failures. 1.3. Latency Amplification Mechanics Three latency amplifiers were identified: Amplifier Root Cause Typical Impact BGP Convergence Lag Route‑flap damping mis‑tuned thresholds +30 s to global reachability TCP RTT Spike Rerouted paths over 2‑3× longer fiber legs +150 ms RTT, 40 % throughput loss DPI‑Induced Handshake Delay Mandatory DPI on encrypted flows +20 ms per TLS handshake The compound effect manifested as a latency cascade: a 30 s BGP outage caused DNS resolvers to fall back to secondary roots, each adding 50‑100 ms; simultaneous TCP RTT spikes then multiplied the effective request‑response latency, causing timeouts at the application layer. 2. The FutureStack Perspective (by Mubashir Arham) "When you build a system that pretends to be the Internet, you must design for the worst‑case network topology, not the average one. Most teams treat BGP as a ‘nice‑to‑have’ routing protocol, but in a disruption it becomes the single point of truth – and the single point of failure when mis‑configured." 2.1. Why This Matters to Us At FutureStack we architect AI‑first, edge‑centric pipelines that rely on sub‑millisecond inference at t...

Powered by Readybot.io

**DEV Community** ## When the Net Went Dark: Architectural Insights …

DEV Community

Cross‑Request KV Cache Exposure in GLM‑5.3 Workers AI: A Deep Dive and SGLang Remediation

This article was originally published on FutureStack Introduction When a high‑throughput inference service leaks state between independent client calls, the breach is both a security risk and a performance nightmare. In late 2024 Cloudflare disclosed a cross‑request KV cache exposure in the GLM‑5.3 model running on Workers AI. The root cause was a subtle configuration mismatch that triggered a bug in SGLang, the lightweight runtime used to orchestrate token generation. Below we dissect the architecture, reproduce the failure mode, and walk through the remediation strategy we implemented at FutureStack. 1. Architectural Deep‑Dive 1.1 GLM‑5.3 on Workers AI – The Stack Layer Technology Role Model GLM‑5.3 (Meta) Large language model (≈ 5.3 B parameters) Runtime SGLang (C++/Rust hybrid) Token‑level scheduler, KV‑cache manager Edge Compute Cloudflare Workers (V8 isolates) Request isolation, network I/O State Store Workers KV (distributed LSM) Persistent key‑value cache for attention KV pages Transport HTTP/2 + binary protobuf Low‑latency inference payload The model is sharded across three V8 isolates. Each isolate holds a local KV cache for the attention mechanism (the so‑called KV pages). The cache lives in Workers KV, which offers eventual consistency and high read‑through performance but no built‑in request‑level isolation. 1.2 KV Cache Memory Model During generation, each transformer layer emits a key and a value matrix for the current token. These matrices are stored as pages (e.g., 64 KB each) in Workers KV under a composite key: kv:{model_id}:{request_id}:{layer_idx}:{token_idx}
model_id – static identifier (e.g., glm5.3). request_id – UUID generated per HTTP request. layer_idx – layer number (0‑N). token_idx – position of the token in the generated sequence. When the runtime needs to attend to past tokens, it fetches the appropriate pages via a batched GET. The latency budget per token is ~2 ms on the edge, so the KV fetch must be cache‑friendly and avoid cross‑request contention. 1.3 SGLang’s KV‑Cache Manager SGLang abstracts the KV store behind a CacheHandle object. Internally it: Pre‑allocates a buffer pool for each request. Maps KV pages to buffer slots via a hash table keyed by the composite string. Releases buffers back to the pool when the request completes. The critical invariant is that no buffer may be reused until the request’s lifecycle ends. The pool is global to the isolate to amortize allocation cost. 1.4 The Configuration Mismatch Workers AI ships with two mutually exclusive modes: Mode sglang.kv_isolation Description Strict true Each request gets a private pool; buffers are zero‑filled on reuse. Relaxed false Buffers are recycled across requests for throughput. A deployment script mistakenly set sglang.kv_isolation = false while the model’s KV‑page TTL was set to 0 (no expiration). The combination meant that after a request finished, its buffers were immediately recycled and could be served to a subsequent request before the KV store had flushed the stale pages. An attacker could therefore infer previous token embeddings by timing the fetch latency or, in the worst case, read the raw KV payload. 2. The SGLang Bug The bug manifested in the CacheHandle::acquire routine: CacheSlot CacheHandle::acquire(const std::string& key) {
auto it = pool.find(key);
if (it != pool.end()) {
// BUG: missing guard that ensures the slot belongs to the current request
return it->second;
}
// allocate new slot …
}
Because the pool key omitted the request_id component when kv_isolation was disabled, two distinct requests could resolve to the same slot. The isolation contract was silently broken, leading to the cross‑request exposure. 3. Remediation Blueprint FutureStack’s remediation strategy had three pillars: Enforce request‑scoped keys – always embed request_id in the KV lookup, regardless of the isolation flag. Introduce a
generation fence – a lightweight epoch counter that invalidates a...

Powered by Readybot.io*

**DEV Community** ## Cross‑Request KV Cache Exposure in GLM‑5.3 Workers …

DEV Community

I Turned Spain's BOE Public Auctions Portal into a Searchable Site

Spain's BOE runs a public auction portal. Every day it lists court-ordered repossessions, tax-agency seizures and notarial sales: flats, garages, warehouses, plots, vehicles. Each listing states an auction value, a minimum bid, a deposit, whether the property is occupied and what charges survive the sale. All of it is public, and none of it is designed to be read by someone deciding whether to bid. I built Subastas BOE around it: a Spanish-language site that turns those listings into a searchable catalogue. The hard part was not the frontend, it was making a government HTML portal behave like a dataset. See it in Spanish: subastas.devetty.es has a filterable search, one page per province, per municipality and per asset type, and a guide to bidding. Two pages per auction The portal exposes a search page and a detail page. The search page returns result blocks; each block has a title, an authority, a file number and a link to the auction. The record itself is split across tabs, and each tab is a separate request with a different ver parameter. So the scrape is one list request with pagination, then two detail requests per auction — one for the auction terms, one for the asset. The list config is short: "list": {
"selector": "li.resultado-busqueda",
"dedupeBy": "id",
"required": ["id", "url_ficha"],
"fields": {
"id": {
"selector": "h3",
"transform": ["collapse", { "regex": "(SUB-[A-Z0-9-]+)" }]
}
}
}
The identifier is the only thing I trust from the list. It looks like SUB-JA-2026-..., and it is what the detail URL needs: https://subastas.boe.es/detalleSubasta.php?idSub={id}&ver=1
https://subastas.boe.es/detalleSubasta.php?idSub={id}&ver=3
Pagination follows a link labelled siguiente, capped at 30 pages and paced 1.5–2.5 seconds apart. Detail requests run one at a time with a 2–4 second delay, and the whole thing is a single daily run, not a tight loop. This is a public service. The cost of being polite is a slower crawl; the cost of not being polite is being blocked, deservedly. Parsing with configuration instead of a parser I did not write selectors in code. The scraper takes a JSON config where each field declares a CSS selector and a chain of transforms. Extracting the minimum bid becomes a line in a file, not a function to debug: "puja_minima": {
"selector": "th:contains('Puja mínima') + td",
"transform": ["collapse", "number"]
}
The portal renders label/value tables, so th:contains(...) + td does most of the work. Dates are the exception: the conclusion date is human text with an ISO timestamp inside parentheses, so I regex the ISO: part for fecha_fin and keep the display text separately. The province problem is a slug problem Asset details carry a province in the source spelling, and Spain's provinces are officially bilingual in several regions. The portal is inconsistent: Araba/Álava, Bizkaia, Gipuzkoa, Illes Balears, Valencia/València, Palmas (Las). Slug those directly and you get three URLs for one place, plus a hub page that splits its own inventory. I resolve it at ingestion, as a chain: "provincia_es": { "op": "clean_regex", "from": "provincia", "pattern": "/^Araba\s\/\s/u" },
"provincia_slug_base": { "op": "clean_regex", "from": "provincia_es", "pattern": "/\s\/.$/u" },
"provincia_alias": { "op": "map", "from": "provincia_slug_base",
"map": { "Bizkaia": "Vizcaya", "Gipuzkoa": "Guipúzcoa", "Illes Balears": "Baleares" } },
"provincia_canon": { "op": "coalesce", "fields": ["provincia_alias", "provincia_slug_base"] },
"provincia_slug": { "op": "slug", "from": "provincia_canon" }
Two regexes and one alias map reduce every variant to one canonical name and one slug. Araba/Álava becomes alava, Valencia/València becomes valencia. The display name stays in provincia; only the URL is canonicalised. That single decision is why the site can cross-link. The same slug works on my sister sites, so an auction in madrid can point at publi...

Powered by Readybot.io

**DEV Community** ## I Turned Spain's BOE Public Auctions Portal …