<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Taranity</title>
    <description>The latest articles on DEV Community by Taranity (@voyagi).</description>
    <link>https://dev.to/voyagi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4101757%2F86d813a3-4dfa-4d5c-ab6e-6acc7d593dfc.png</url>
      <title>DEV Community: Taranity</title>
      <link>https://dev.to/voyagi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/voyagi"/>
    <language>en</language>
    <item>
      <title>Checking EU tender criteria with a model whose every quote has to be found word for word</title>
      <dc:creator>Taranity</dc:creator>
      <pubDate>Tue, 27 Oct 2026 19:46:26 +0000</pubDate>
      <link>https://dev.to/voyagi/checking-eu-tender-criteria-with-a-model-whose-every-quote-has-to-be-found-word-for-word-5dmi</link>
      <guid>https://dev.to/voyagi/checking-eu-tender-criteria-with-a-model-whose-every-quote-has-to-be-found-word-for-word-5dmi</guid>
      <description>&lt;p&gt;A small IT firm can lose two days on a bid before it finds the one line that rules it out: a minimum yearly turnover, a staff count per role, a certificate. That line sits in the tender's selection criteria, in German, Dutch or French, and when the notice sends you to the procurement documents it can be fifty pages in.&lt;/p&gt;

&lt;p&gt;I built Pagefifty for the Nebius x NVIDIA Global AI Hackathon to read that line first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;A capture command asks TED, the EU's tender database, for open IT notices that buyers in the Netherlands, Belgium and Germany published in the last 14 days. For every lot that publishes its selection criteria, NVIDIA's Nemotron 3 Super on Nebius Token Factory reads the clauses and returns typed requirements as JSON, each with a verbatim quote. Code compares those requirements with the firm's profile, in the visitor's browser, and every lot gets "Go", "Not met alone" or "Ask first", with the deciding clause quoted under it and a link to the notice.&lt;/p&gt;

&lt;p&gt;Take notice 713597-2026, a German federal framework contract. It asks for EUR 1,400,000 a year in turnover in the field. The demo firm made EUR 1,090,000, 1,210,000 and 1,240,000, so the lot reads "Not met alone", and the page explains that Article 63 of the EU procurement directive lets another firm lend that turnover or bid jointly. For a small firm, "not on your own" is a different answer from "no".&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea that shaped it
&lt;/h2&gt;

&lt;p&gt;The model never gets the last word. Code has to find each quote in the clause it came from, word for word once spacing, line breaks, hyphens and curly quote marks are evened out. A quote it can't find, or one shorter than 12 characters, is refused, and the criterion shows as "Not read". Every amount, count and year is parsed by code from a verified quote, never from the model's own fields.&lt;/p&gt;

&lt;p&gt;On 22 October the model gave 1,865 quotes across 288 lots, and code refused 76 of them. Notice 699142-2026 shows what that costs: the model turned a clause's double quotes into single ones, so none of its three quotes matched, and a staff count that was actually right reads "Not read". I'd still rather show that than a figure nobody can trace back to the tender.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Thinking is on by default, and it costs. One German lot used 4,101 reasoning tokens, 22.4 seconds and USD 0.00532. With &lt;code&gt;enable_thinking: false&lt;/code&gt; the same lot took 6.1 seconds and USD 0.00181 and still came back as valid JSON. A Dutch lot with thinking on spent its whole 8,000-token budget thinking and returned nothing.&lt;/li&gt;
&lt;li&gt;The server didn't enforce my JSON schema. One French reply broke JSON with a raw tab copied straight out of the tender. Every reply now goes through Zod, a raw tab becomes a space, and a failed reply gets one repair retry with the validation errors in the prompt.&lt;/li&gt;
&lt;li&gt;The full read cost USD 0.275838 for 225 calls, 293,177 tokens in and 208,761 out. That's under a tenth of a US cent per lot.&lt;/li&gt;
&lt;li&gt;Most of the time the rule really is on page fifty. 123 of the 323 notices published no selection criteria in the notice at all, so this version answers "Ask first" for them, and reading the document packs comes next.&lt;/li&gt;
&lt;li&gt;A web search is a lead, not evidence. One Tavily search per notice on the buyer's name kept 658 of 1,000 results after a tender-word filter. They show apart, unchecked, and never change an answer. For 713597-2026 my query brought back five pages about Florida's time zone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it isn't yet
&lt;/h2&gt;

&lt;p&gt;There's one live model run so far, so I can't yet say how much the reading varies between runs. The quote check proves the words are the tender's own, not that the model read them right. And the demo firm is invented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Try it, no account: &lt;a href="https://pagefifty.atfyigtaran.workers.dev" rel="noopener noreferrer"&gt;https://pagefifty.atfyigtaran.workers.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo video: &lt;a href="https://youtu.be/NZ8mjAI7mMo" rel="noopener noreferrer"&gt;https://youtu.be/NZ8mjAI7mMo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code, MIT: &lt;a href="https://github.com/voyagi/pagefifty" rel="noopener noreferrer"&gt;https://github.com/voyagi/pagefifty&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Devpost: &lt;a href="https://devpost.com/software/pagefifty" rel="noopener noreferrer"&gt;https://devpost.com/software/pagefifty&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>showdev</category>
      <category>svelte</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>Pausing the one Amazon SES lane that's bouncing, and proving the pause took</title>
      <dc:creator>Taranity</dc:creator>
      <pubDate>Sun, 25 Oct 2026 11:24:26 +0000</pubDate>
      <link>https://dev.to/voyagi/pausing-the-one-amazon-ses-lane-thats-bouncing-and-proving-the-pause-took-3b7j</link>
      <guid>https://dev.to/voyagi/pausing-the-one-amazon-ses-lane-thats-bouncing-and-proving-the-pause-took-3b7j</guid>
      <description>&lt;p&gt;A small shop usually sends all its mail through one Amazon SES account: receipts, password resets, the newsletter. SES judges bounces across the whole account, so one stale list imported into the newsletter can take the password resets down with it.&lt;/p&gt;

&lt;p&gt;I built Fothering for the AWS CDS Agentic AI Partner Hackathon to stop that. The name comes from seamanship. Fothering a ship means drawing a sail under the hull over a leak so she can keep going, and it's how HMS Endeavour got off the Great Barrier Reef in June 1770.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## What it does&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each kind of mail goes through its own SES configuration set, and SES publishes each set's events to EventBridge. Fothering keeps a minimized copy of every event (the recipient's domain and a keyed hash, never the address, expiring after 30 days) and looks at every lane every five minutes. A lane opens an incident at 5% permanent bounces or 0.1% complaints, over at least 100 sends and 3 events.&lt;/p&gt;

&lt;p&gt;It then pauses that one set with &lt;code&gt;PutConfigurationSetSendingOptions&lt;/code&gt;, reads the setting back with &lt;code&gt;GetConfigurationSet&lt;/code&gt;, checks about ten minutes later that nothing got through, and emails the operator the evidence and the command to reopen. It never pauses a transactional lane or reopens anything by itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## Where the agent sits&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent runs on Amazon Bedrock AgentCore Runtime in eu-central-1 and calls Claude Sonnet 4.6 through Bedrock's EU inference profile. It gets one evidence record and the lane that code says may be closed. It confirms that lane or answers unknown, and it writes one paragraph for the email, which the email labels as the agent's. It holds no tool that changes anything. A separate function checks the decision again before it touches SES, and an unknown closes nothing.&lt;/p&gt;

&lt;p&gt;I'd rather the guard do nothing than guess. Pausing the wrong lane is the exact failure it exists to prevent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## The live run&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On 22 October I deployed the four stacks into a real AWS account and sent the scenario to the SES mailbox simulator. On the bulk lane 20 of 100 recipient-sends bounced for good, and all 20 came from the batch &lt;code&gt;stale-import&lt;/code&gt;. SES answered &lt;code&gt;SendingEnabled false&lt;/code&gt; for &lt;code&gt;fothering-bulk&lt;/code&gt;. A test send on the paused set was refused with &lt;code&gt;SendingPausedException&lt;/code&gt;, while the transactional set kept accepting mail. I reopened it from the email's command and removed every stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## Things I learned&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first live run closed the lane and then failed to send its own email, with &lt;code&gt;AccessDeniedException&lt;/code&gt;. In the SES sandbox, &lt;code&gt;ses:SendEmail&lt;/code&gt; is also authorized against the recipient's identity, not only the sender's. My role named the From domain and the From address, never the operator's address. After granting the send on that identity too, the re-run sent the email on its first attempt.&lt;/p&gt;

&lt;p&gt;Bedrock refused Claude Sonnet 5 on this account with "not available for this account", although the use case form was on file. Access to a Marketplace model has four parts (region, entitlement, authorization and agreement), and &lt;code&gt;aws bedrock get-foundation-model-availability&lt;/code&gt; shows each one. The agent moved to Sonnet 4.6.&lt;/p&gt;

&lt;p&gt;In the first live run the model's whole part was one call, 2,276 tokens in and 245 out.&lt;/p&gt;

&lt;p&gt;A ratio needs a minimum count beside it. At 100 sends a single complaint already reads 1%, ten times the complaint threshold, and one report is not evidence that a stream went wrong. So a lane needs 3 events before its ratio counts.&lt;/p&gt;

&lt;p&gt;DynamoDB's TTL deletes an expired item "typically within a few days", and until then it still shows up in reads. Every query filters on expiry, and the privacy page says records are deleted within a few days after 30 days, not on day 30.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## What it isn't yet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It has only run against the mailbox simulator, and only one scenario ran live, so I'm not putting a performance number on it until there are three live runs. AWS also documents its own per-set pause, a CloudWatch alarm that notifies SNS and triggers a Lambda function. Fothering adds the named cause, the read-back, and a refusal to close anything when the cause isn't settled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## Links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;See it run, nothing to install:&lt;/strong&gt; &lt;a href="https://fothering.taranity.com/replay/" rel="noopener noreferrer"&gt;https://fothering.taranity.com/replay/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo video:&lt;/strong&gt; &lt;a href="https://youtu.be/lt8Ug9c8zhk" rel="noopener noreferrer"&gt;https://youtu.be/lt8Ug9c8zhk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code, Apache 2.0:&lt;/strong&gt; &lt;a href="https://github.com/voyagi/fothering" rel="noopener noreferrer"&gt;https://github.com/voyagi/fothering&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Devpost:&lt;/strong&gt; &lt;a href="https://devpost.com/software/fothering" rel="noopener noreferrer"&gt;https://devpost.com/software/fothering&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>email</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building a kitchen temperature log that refuses "fine"</title>
      <dc:creator>Taranity</dc:creator>
      <pubDate>Tue, 20 Oct 2026 18:10:40 +0000</pubDate>
      <link>https://dev.to/voyagi/building-a-kitchen-temperature-log-that-refuses-fine-2n0m</link>
      <guid>https://dev.to/voyagi/building-a-kitchen-temperature-log-that-refuses-fine-2n0m</guid>
      <description>&lt;p&gt;A paper temperature log takes any line you write on it. On a sheet taped to a walk-in door, "fine" fits as easily as "38", and the sheet can't tell you which of the two was actually measured.&lt;/p&gt;

&lt;p&gt;I built Tolsey for the Alexa+ track of Amazon's Build, Ship, Shape hackathon. It's a food safety log for small restaurant kitchens, and a cook fills it in by voice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## How a reading gets logged&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A cook with gloved hands says "the walk-in reads 38". Tolsey works out which check point that is, which rule applies to it and whether 38 passes, then appends one line to the kitchen's record for the day, with the cook's initials and the rule's source. The reply is a few words for Alexa to say, "Walk-in, 38, in range, AB", and the screen shows the line. Say "the soup well reads 120" and it comes back OUT OF RANGE, because the Food Code wants hot food held at 135°F or more.&lt;/p&gt;

&lt;p&gt;Under that sits a self-hosted MCP server (spec 2025-11-25, Streamable HTTP) on Cloudflare Workers, with the record in D1 and two tools, &lt;code&gt;log_reading&lt;/code&gt; and &lt;code&gt;due_now&lt;/code&gt;. The Alexa+ MCP toolkit is a preview for select partners, so the Echo Show in the demo is a simulated page. It is still a real MCP client of the deployed server, and it sends &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt; and &lt;code&gt;tools/call&lt;/code&gt; the way any host would.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## What it won't log&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tolsey turns down three kinds of answer: a check with no number ("reach-in two is fine"), a number wrapped in a hedge ("under 41", "about 40", "38 or 39"), and a number whose commonly misheard twin would flip the verdict. Each refusal is saved as an open question with its own id. If the cook answers within two minutes, "38" on its own closes it. A paper log has nowhere to put a question like that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## Notes from the build&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The voice host fills in the value for you. Before it calls a tool, a host pulls the number out of the sentence, and for "under 41" it may well send 41. A server that only read the value would store a limit as a reading, so Tolsey keeps the cook's own words beside the value and checks them a second time.&lt;/p&gt;

&lt;p&gt;Asking for digits only pays off when the answer would change. Fifteen and fifty sound alike across a loud kitchen, and at 50°F the walk-in fails, so "walk-in 15" gets "say it one digit at a time". On hot food, 160 and 116 get the same treatment. A log that asked every time would be ignored by lunch.&lt;/p&gt;

&lt;p&gt;Mutation testing is slow, about four and a half hours for a full run, and I'd still keep it. Stryker injected 2,350 bugs into the decision and server code, and the tests caught 2,329 of them, 99.11%. The same conformance suite runs in process, in local workerd and against the live Worker with no changes between them, and on 2026-10-18 it passed 30 of 30 against the deployed server.&lt;/p&gt;

&lt;p&gt;One surprise came from Cloudflare. I serve the MCP endpoint and the web page from one Worker, in single-page-app mode with a &lt;code&gt;run_worker_first&lt;/code&gt; list. A POST to any path that isn't on that list never reaches the Worker: the asset layer answers 405 with an empty body, and the Worker logs nothing. The docs don't mention it, so I opened a pull request that adds a note: &lt;a href="https://github.com/cloudflare/cloudflare-docs/pull/34475" rel="noopener noreferrer"&gt;https://github.com/cloudflare/cloudflare-docs/pull/34475&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cut I mind most:&lt;/strong&gt; an out-of-range reading should open a fix with a deadline, and the record should follow it to closed or overdue. That's the next build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;- Live, no install (type numbers as digits):&lt;/strong&gt; &lt;a href="https://tolsey.taranity.com" rel="noopener noreferrer"&gt;https://tolsey.taranity.com&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;- Demo video:&lt;/strong&gt; &lt;a href="https://youtu.be/zx2u-TCy7jI" rel="noopener noreferrer"&gt;https://youtu.be/zx2u-TCy7jI&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;- Code, MIT licence:&lt;/strong&gt; &lt;a href="https://github.com/voyagi/tolsey" rel="noopener noreferrer"&gt;https://github.com/voyagi/tolsey&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;- Devpost:&lt;/strong&gt; &lt;a href="https://devpost.com/software/tolsey" rel="noopener noreferrer"&gt;https://devpost.com/software/tolsey&lt;/a&gt;&lt;/p&gt;

</description>
      <category>hackathon</category>
      <category>iot</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Agent memory that tells you when it couldn't check</title>
      <dc:creator>Taranity</dc:creator>
      <pubDate>Sat, 26 Sep 2026 19:28:29 +0000</pubDate>
      <link>https://dev.to/voyagi/agent-memory-that-tells-you-when-it-couldnt-check-2aao</link>
      <guid>https://dev.to/voyagi/agent-memory-that-tells-you-when-it-couldnt-check-2aao</guid>
      <description>&lt;p&gt;An on-call agent that answers "no prior incidents" might be telling you the archive is empty. It might also be telling you the embedding call timed out, and most memory stores return both as the same empty list.&lt;/p&gt;

&lt;p&gt;I built Throughline in August for the CockroachDB x AWS hackathon on agentic memory. It didn't place. My guess at why is at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Throughline is an incident-response agent with a memory layer you can audit. Memories are typed, and the type sets how fast they decay. "The primary is db-7" is an entity fact with a 14-day half-life. "Restarting the pods did not help" is a rejected hypothesis, and it keeps its value for a year. Most systems throw that second kind away, and it's the cheapest useful thing you can tell someone mid-outage.&lt;/p&gt;

&lt;p&gt;Every recall comes back with a receipt: the retrieval path that actually ran, how many candidates were examined, what was excluded and under which rule, and a coverage verdict of COVERED, PARTIAL or UNKNOWN. If the search couldn't run, the verdict is UNKNOWN, and a guard at the boundary makes it an error to report that as "nothing found". Ranking is computed in code. The model (Claude Haiku on Bedrock) writes the answer around the results and never produces the number that ordered them.&lt;/p&gt;

&lt;p&gt;One limit, up front. With no cloud keys, recall uses a small local embedder that matches words, not meaning. Ask it a question in French about a memory written in English and it will say it searched the whole workspace and found nothing relevant. That's true: it compared them, and they share no words. The receipt proves the search ran. It can't prove the search was clever. Matching meaning is the hosted embedder's job, and on Bedrock that's Titan.&lt;/p&gt;

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

&lt;p&gt;Vector indexing works on CockroachDB's free Basic tier. On 2026-08-03, on v26.2.1, the cluster setting read true and &lt;code&gt;CREATE VECTOR INDEX&lt;/code&gt; completed. I couldn't find that written down anywhere.&lt;/p&gt;

&lt;p&gt;My first index, on the embedding column alone, was close to useless. An unfiltered nearest-neighbour query used it. Every query recall actually runs has a filter, though, and &lt;code&gt;WHERE workspace_id = $1&lt;/code&gt; planned as a full scan. The docs do say filters are only accelerated on prefix columns. I hadn't read that as "the planner skips the index entirely". The fix was an index on &lt;code&gt;(workspace_id, is_live, embedding)&lt;/code&gt;, where &lt;code&gt;is_live&lt;/code&gt; is a stored computed column so it can't drift from the eviction timestamp it comes from. The capability probe now reads the query plan instead of trusting that an index exists.&lt;/p&gt;

&lt;p&gt;I also wrote a client for CockroachDB's managed MCP server, as a second channel that has to agree with the direct one. It never made it into the running demo, but probing the live server paid off. Every failure I could produce came back as HTTP 200, with the error in the JSON-RPC body and no result. A client written the obvious way, &lt;code&gt;rows ?? []&lt;/code&gt;, turns an outage into "that row doesn't exist", the exact failure Throughline exists to catch. And &lt;code&gt;select_query&lt;/code&gt; quietly adds &lt;code&gt;LIMIT 25&lt;/code&gt; when you don't give it one.&lt;/p&gt;

&lt;p&gt;The bug that nearly poisoned the demo was two vector spaces in one column. I'd seeded the demo rows with the local embedder while recall went through Titan. Cosine similarity across two embedders is noise, and nothing throws. Seeding now refuses an embedder that differs from recall's, and says why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I think it lost
&lt;/h2&gt;

&lt;p&gt;There were 444 entries and three prizes. The winner, Anchor, was also an on-call agent with an Unknown verdict, and its values are close to mine. Its memory and its actions commit in the same transaction, though, so CockroachDB is the mechanism rather than the storage. I kept my memory layer independent of the database on purpose. That's the right call for a library and the wrong one for a contest that asks what the database does for you. I also never deployed the public demo URL the rules asked for (the AWS deploy was planned and never written), and I offered a test count where the winners offered measured comparisons against a baseline.&lt;/p&gt;

&lt;p&gt;The demo is 2:47, silent, with captions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/voyagi/throughline" rel="noopener noreferrer"&gt;https://github.com/voyagi/throughline&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo: &lt;a href="https://www.youtube.com/watch?v=Efd7RWp-_JY" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=Efd7RWp-_JY&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Devpost: &lt;a href="https://devpost.com/software/throughline-fdr0o1" rel="noopener noreferrer"&gt;https://devpost.com/software/throughline-fdr0o1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CockroachDB docs issue for both findings: &lt;a href="https://github.com/cockroachdb/docs/issues/23467" rel="noopener noreferrer"&gt;https://github.com/cockroachdb/docs/issues/23467&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>database</category>
    </item>
    <item>
      <title>Agents for Humans: an agent that refuses to guess what your letter says</title>
      <dc:creator>Taranity</dc:creator>
      <pubDate>Mon, 14 Sep 2026 17:09:18 +0000</pubDate>
      <link>https://dev.to/voyagi/agents-for-humans-an-agent-that-refuses-to-guess-what-your-letter-says-2f98</link>
      <guid>https://dev.to/voyagi/agents-for-humans-an-agent-that-refuses-to-guess-what-your-letter-says-2f98</guid>
      <description>&lt;p&gt;Every country sends people letters they can't read. Many answer it the same way: a free help desk, usually in a library, where a volunteer sits down with you and the envelope. The Netherlands runs 861 of them. I live here, these letters come to me too, and I don't read Dutch. The first ten minutes at that desk decide everything: what is this, how bad is it, what happens if I ignore it, what do I do, and by when.&lt;/p&gt;

&lt;p&gt;Plainletter is my entry for the Good Neighbor track. It's the agent behind that desk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;A volunteer photographs or uploads the letter. Plainletter names the sender and the kind of letter, pulls out every amount, reference number and deadline, and marks each one on the letter with a numeral in the margin. It explains the letter side by side in Dutch and in the visitor's own language, counts the days left, lays out the next steps with official routes, drafts the reply when that's the right move, and prints a desk card with a calendar reminder.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule: nothing that isn't in the letter
&lt;/h2&gt;

&lt;p&gt;Five stages are a model speaking. Everything between them is plain Python, including the verifier that decides whether a reading may reach a person. For every date, amount and reference, it checks that the cited passage really stands in the letter and that the value follows from it.&lt;/p&gt;

&lt;p&gt;Strands Agents made the enforcement cheap to build. Every structured answer is a tool call, so I put an intervention on that boundary. A date the verifier didn't ground is refused before it exists, and the refusal goes back to the model as the tool's result, so it writes again. Official routes come from a knowledge base through a tool, and each entry carries the page it was read on. A photographed letter is transcribed by one turn and read by a separate one, which means an invented fact would also have to appear in a transcript written without it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What running it for real taught me
&lt;/h2&gt;

&lt;p&gt;The first live run on a photographed page did the thing I built it for, and I only believed it once I watched it happen. A line was clipped by the fold. The transcription turn wrote an unreadable marker instead of guessing. The extractor quietly filled the gap in anyway, and the verifier sided with the transcript, refused the fact, and handed the letter to a person.&lt;/p&gt;

&lt;p&gt;Then I folded a health insurer letter, put it in shadow and photographed it in a hurry. Four facts were refused. One was EUR 149,38 where the page says EUR 149,35. Nobody at a desk would catch that by eye.&lt;/p&gt;

&lt;p&gt;It isn't finished. On a letter where part of the reference number can't be read, the desk asks the visitor for it, but the payment step still quotes the number with its gap. A volunteer catches that at the counter today. Holding that step until the number is confirmed is next on my list.&lt;/p&gt;

&lt;p&gt;The guard only knew Dutch month names at first, so a wrong date written in Ukrainian would have passed unread. It reads five languages now.&lt;/p&gt;

&lt;p&gt;My first deployed run put all sixteen lines of a letter into the CloudWatch traces: name, street, reference, amounts. That was fixed and re-checked, and the prompts in the traces read REDACTED.&lt;/p&gt;

&lt;p&gt;Money was the last surprise. One reading is six Bedrock calls and costs about USD 0.107. The daily ceiling I had first set, 200 readings, meant one bad day could cost more than my whole monthly budget alarm of 20 dollars. It's 50 now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it runs
&lt;/h2&gt;

&lt;p&gt;Strands Agents SDK on Amazon Bedrock with EU inference, AgentCore Runtime and Memory in Frankfurt, and a Next.js console on Vercel. Memory only happens with the visitor's consent, and then only checked, masked values for thirty days.&lt;/p&gt;

&lt;p&gt;A second country would be one locale file, a knowledge base for its senders, and sample letters to prove it. A test fails if a Dutch word leaks outside that boundary.&lt;/p&gt;

&lt;p&gt;Try the desk: &lt;a href="https://plainletter-web.vercel.app/desk" rel="noopener noreferrer"&gt;https://plainletter-web.vercel.app/desk&lt;/a&gt;&lt;br&gt;
Code, with two real readings you can open without installing anything: &lt;a href="https://github.com/voyagi/plainletter" rel="noopener noreferrer"&gt;https://github.com/voyagi/plainletter&lt;/a&gt;&lt;br&gt;
Demo video: &lt;a href="https://youtu.be/sXkQvS_dTos" rel="noopener noreferrer"&gt;https://youtu.be/sXkQvS_dTos&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  AgentsforHumans
&lt;/h1&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Ringbolt: an on-call line that has to hear you say it</title>
      <dc:creator>Taranity</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:44:04 +0000</pubDate>
      <link>https://dev.to/voyagi/ringbolt-an-on-call-line-that-has-to-hear-you-say-it-497c</link>
      <guid>https://dev.to/voyagi/ringbolt-an-on-call-line-that-has-to-hear-you-say-it-497c</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;On-call tools can already ring you. PagerDuty's phone alert is a recording that asks you to press 4 to acknowledge and 6 to resolve, and you still have to get up and find a laptop to learn what actually broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ringbolt does
&lt;/h2&gt;

&lt;p&gt;Any monitor posts a plain JSON alert. Policy decides whether it's worth waking somebody, going by the service's severity threshold, its quiet hours and whether the same problem already rang a phone. If it is, Ringbolt places a call through the CALL-E API to whoever is on the rota. On the call you can ask what broke and what else is affected, and then say what to do. The whole exchange lands in one audit record: the transcript, the decision, who authorized it, what ran, and the state before and after.&lt;/p&gt;

&lt;p&gt;It runs on Cloudflare Workers, with one Durable Object per incident holding the state machine and the escalation timer, and D1 for storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea that shaped it
&lt;/h2&gt;

&lt;p&gt;Handing a phone call the power to change production is only reasonable if the refusals are strict, so most of the design is about what does not run. CALL-E delivers its terminal webhook unsigned, so Ringbolt never trusts it and fetches the call again under its own key before acting. The decision has to validate against a schema, clear a confidence floor and name an action the service's policy already allows. For anything destructive, the confirmation phrase has to appear as whole words in a transcript turn from the responder, and "don't roll it back" does not count as "roll it back".&lt;/p&gt;

&lt;p&gt;A call where the responder was never heard is refused outright, whatever the confidence score says. The demo carries an example of exactly that: a call that came back completed, at high confidence, with a valid decision to change production, where not one word from the person who answered was transcribed. It never ran.&lt;/p&gt;

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

&lt;p&gt;A create that times out isn't a create that didn't happen. On 22 August my build created 23 separate call tasks in half an hour, and each one is billed whether it connects or not. Two rules came out of that. A failed create is retried once under the same idempotency key, because a repeat under a fresh key bills as a second call to the same person. And there's now a hard ceiling of three real calls in ten minutes, because every one of those 23 was a different logical call, so no per-call check could have stopped any of them.&lt;/p&gt;

&lt;p&gt;Transcription is lossy in both directions. The transcript check will sometimes refuse a call someone really did authorize. I took that trade on purpose, because the refusal falls toward calling the next person on the rota and never toward an unauthorized change.&lt;/p&gt;

&lt;p&gt;The last one came from the provider's region table, which lists the Netherlands with English. The call setup I used was still refused, with "Calls to the Netherlands in English are not supported for this call setup". That's why the public demo runs the same loop against a local stand-in of their API, and why one contract suite runs every case against both the stand-in and the real adapter, so the two can't drift apart. There are 555 tests in total.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The hosted demo has a service called dockside that exists to be broken. Press the button and watch the alert, the call, the transcript and the rollback. Nothing you press there can ring a phone.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demo: &lt;a href="https://ringbolt.taranity.com" rel="noopener noreferrer"&gt;https://ringbolt.taranity.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/voyagi/ringbolt" rel="noopener noreferrer"&gt;https://github.com/voyagi/ringbolt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Video: &lt;a href="https://www.youtube.com/watch?v=EIm87clsks8" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=EIm87clsks8&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The same walkthrough in words: &lt;a href="https://github.com/voyagi/ringbolt/blob/main/examples/demo-transcript.md" rel="noopener noreferrer"&gt;https://github.com/voyagi/ringbolt/blob/main/examples/demo-transcript.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built for the CALL-E hackathon.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>monitoring</category>
      <category>sre</category>
    </item>
    <item>
      <title>Seven designs, one URL: how I built a site that changes its whole appearance without changing its address</title>
      <dc:creator>Taranity</dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:04:25 +0000</pubDate>
      <link>https://dev.to/voyagi/seven-designs-one-url-how-i-built-a-site-that-changes-its-whole-appearance-without-changing-its-h0l</link>
      <guid>https://dev.to/voyagi/seven-designs-one-url-how-i-built-a-site-that-changes-its-whole-appearance-without-changing-its-h0l</guid>
      <description>&lt;p&gt;&lt;em&gt;Update, October 2026: the site has changed since I wrote this, and the switcher doesn't carry all seven designs any more. The post describes it as it was in September.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most sites that offer a theme swap a few colour variables. I wanted something harder to fake: seven genuinely different designs of the same content, switchable in place, with the URL never changing and search engines still seeing exactly one canonical page.&lt;/p&gt;

&lt;p&gt;Here is what that took, and the four things I got wrong on the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;taranity.com is a static Astro build on Cloudflare Pages. It renders seven complete design trees from the same content: seven typographic systems, seven page skeletons, seven motion grammars. A visitor picks one, a cookie remembers it, and a Pages Function at the edge serves that design's prebuilt HTML at the same canonical URL. Crawlers get the default. There is exactly one URL per piece of content, and the variant trees carry noindex.&lt;/p&gt;

&lt;p&gt;Seventy pages, all prerendered. No server rendering, no per-request work beyond reading a cookie and choosing a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one idea that shaped it
&lt;/h2&gt;

&lt;p&gt;Do the branching at the edge, not in the browser.&lt;/p&gt;

&lt;p&gt;The obvious approach is to ship every design's CSS and swap a class on the root element. That means every visitor downloads seven designs to use one, and the flash before hydration is unavoidable. Instead each design is a separate prebuilt tree, and a tiny middleware picks the tree. The visitor downloads one design. The address bar never moves. And because the swap happens before any HTML reaches the browser, there is nothing to flash.&lt;/p&gt;

&lt;p&gt;The cost is honest and worth naming: the response now varies by cookie, so the caching has to say so. Every response carries Vary: Cookie, and the variant responses are marked private. Getting that wrong would mean serving one visitor's design to another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things I got wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;My type checking had never actually run.&lt;/strong&gt; It passed on my machine for months. It could not pass anywhere else. The command ran the type checker before the step that generates the content types, and that generated folder is gitignored, so my machine had a stale copy and a fresh clone had nothing. The first time CI ran on a clean checkout, it went red immediately. If a gate reads something git does not carry, it has not been tested until CI runs it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A test key does not remove a third party from your tests.&lt;/strong&gt; My end to end tests use Cloudflare's published dummy Turnstile key, the one documented as always passing. One morning the contact suite dropped from 75 of 75 to 42 of 75 with no code change. The dummy key removes the scoring, not the browser integrity handshake, so the suite was still hostage to Cloudflare's opinion of my machine. Thirty three failures, and not one of them was about my site.&lt;/p&gt;

&lt;p&gt;The fix was structural rather than clever: an explicit declaration that the live challenge is unavailable, which makes the form tests run against a stand in value while the widget checks report honestly. Without the declaration a refusal still fails. A banner says in plain words that the widget was not exercised, so a green run can never quietly mean the bot check is broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cleanup block is not a guard.&lt;/strong&gt; That declaration is passed as a small file, removed when the run finishes. A review caught what I had missed: a run interrupted before the cleanup leaves the file behind, and the next run reads it and opts itself out silently. I had written a comment describing that exact hazard and then guarded only the exit path. Interrupts skip the exit path. It now clears the file on the way in too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every dependency advisory had a smaller fix than I reached for.&lt;/strong&gt; Four advisories, including one rated critical. In every single case the parent package's own declared version range already permitted the patched release, and only the lockfile was pinning something older. No overrides, no major upgrades, one lockfile change each. Worth checking before proposing anything with blast radius.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it landed
&lt;/h2&gt;

&lt;p&gt;411 tests across 29 files. Zero dependency advisories at any severity. A strict content security policy with exactly one pinned inline script hash, which survived four dependency upgrades because I re-measured it against each one. An accessibility audit that drives a real browser across every page type in every design in both colour modes: 50 checks, zero violations, with contrast measured from painted pixels rather than computed styles, because a decorative layer over a control will fool a computed style.&lt;/p&gt;

&lt;p&gt;The accessibility statement is the part I would point at. It says what is tested, and then it says what is not: no independent audit, no screen reader testing, and that the audit runs on my machine rather than automatically. That last line is there because it used to claim otherwise, and that was not true.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://taranity.com" rel="noopener noreferrer"&gt;https://taranity.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>bumpwarden: a dependency-triage agent that never merges</title>
      <dc:creator>Taranity</dc:creator>
      <pubDate>Sun, 30 Aug 2026 20:04:36 +0000</pubDate>
      <link>https://dev.to/voyagi/bumpwarden-a-dependency-triage-agent-that-never-merges-2ci7</link>
      <guid>https://dev.to/voyagi/bumpwarden-a-dependency-triage-agent-that-never-merges-2ci7</guid>
      <description>&lt;p&gt;Every neglected repository has the same pile: seven Dependabot pull requests, each one a question nobody has time to answer. Which of these breaks the build? That question is the actual work. The pull requests are just its packaging.&lt;/p&gt;

&lt;p&gt;I built bumpwarden as my entry to Google's All Things Agentic hackathon, and this writeup is part of that entry. It is a background agent that does the triage itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Twice a day, Cloud Scheduler wakes a Cloud Run service. It reads the manifest and lockfile from GitHub, resolves candidate versions from the npm registry and deps.dev, and scores every pending bump from 0 to 100 against a fixed rubric: nine factors, published weights. Then it acts. A clear bump becomes a pull request that edits package.json and nothing else. A caution bump becomes an issue. A risky one becomes a hold issue with a migration plan. Run it again tomorrow and it updates the same items in place instead of opening more. There is no merge action anywhere in its policy. A person merges, or nobody does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arithmetic decides, the model explains
&lt;/h2&gt;

&lt;p&gt;That one rule shaped the whole design. The score comes from checkable facts: semver distance, release age, advisories, deprecation, breaking markers in the release notes, engine and peer ranges, and whether this repository's own code calls a symbol the release evidence says changed. Gemini (gemini-3.5-flash, through an ADK agent) writes the brief: what changed upstream, what breaks in this codebase down to file and line, how to migrate, and how confident it is. Every claim is checked against the material the model was handed, and a claim that cannot be traced is dropped rather than shown. The model cannot move the score and has no write tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building it taught me
&lt;/h2&gt;

&lt;p&gt;The free tier is a design constraint, and a good one. Five model requests a minute, twenty a day, and a brief costs two. So briefs go out two at a time, riskiest first, paced against a sliding minute, and cached by bump and rubric version. A repeat run costs zero model requests, which also means a judge pressing the button twice gets an idempotent demo instead of a quota error.&lt;/p&gt;

&lt;p&gt;/healthz is not yours on Cloud Run. Google's frontend reserves the path and answers before your app ever sees the request. Every test passes locally, and the deployed route is dead anyway. I renamed mine to /health.&lt;/p&gt;

&lt;p&gt;Do not read npm packuments whole. @prisma/client's weighs 68 MB. Reading the two small per-version documents instead took a full run from 197 MB of registry traffic down to 30.&lt;/p&gt;

&lt;p&gt;GitHub drops labels silently when the token cannot push. No error, just an issue created without its labels. And if your idempotency looks issues up by label, every run now duplicates its own work. My test double modeled the documented API, not this quirk, which is exactly why 785 green tests missed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it run
&lt;/h2&gt;

&lt;p&gt;The dashboard, the scoring rubric and the audit log are public: &lt;a href="https://bumpwarden-729965149669.europe-west1.run.app" rel="noopener noreferrer"&gt;https://bumpwarden-729965149669.europe-west1.run.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code: &lt;a href="https://github.com/voyagi/bumpwarden" rel="noopener noreferrer"&gt;https://github.com/voyagi/bumpwarden&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The four-minute demo, with the run itself one unbroken take: &lt;a href="https://youtu.be/5WJTfWbNeQ8" rel="noopener noreferrer"&gt;https://youtu.be/5WJTfWbNeQ8&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>automation</category>
      <category>devops</category>
      <category>github</category>
    </item>
  </channel>
</rss>
