We already had two working ways to say yes or no to something our own systems wanted to do: tappable Approve/Reject buttons in Telegram, and a spoken confirm gate on the iPad running asuna, our voice-first operator, that speaks a request aloud and waits for sí or no. Both were real, tested, and used. They just didn't know about each other. Answering on one channel had no effect on the other, so the same request could sit open on your phone and never reach the tablet, or the reverse. This week we closed that gap.
The Telegram half has existed for a while: inline buttons on a message, a callback handler that records the verdict, and a small HTTP route any of our systems can call to push a new item onto the queue. The asuna half is newer and, in its way, more interesting — a gate that broadcasts a request to voice and to the Pebble overlay, speaks it out loud, blocks until it hears sí or no, times out to a denial if nobody answers, and writes every decision into asuna's own hash-chain audit log. Neither piece needed to be rebuilt. The problem was that they were two separate queues with two separate ideas of what was pending.
We kept ops-worker's existing Redis-backed approval store as the single source of truth and added two routes to it: one that lists what's pending, one that accepts a decision. asuna doesn't get anything pushed to it. It polls — the same pull-only posture it already uses for every other piece of ecosystem data it reads — and when a fresh item shows up, it speaks the title and summary and waits for an answer, exactly like it already did for its own local confirmations. Telegram shows the same item as a card, independently, at the same time. Whichever channel answers first wins, using the decision lock the Telegram flow already had; the other channel simply stops seeing the item as pending.
asuna never gets told what to approve. It goes and checks. We weren't willing to trade that away for a few seconds of latency, even though pushing would have been the simpler build.
The trickiest part wasn't the polling loop — it was making sure a voice decision doesn't leave a stale button card behind. Originally, the Telegram message only got edited — buttons stripped, verdict stamped — when the Telegram callback itself fired. We moved that edit into the shared decision path instead, so a card finalizes no matter which channel actually decided it. Without that fix, saying sí out loud would resolve the request but leave a live, tappable Approve button sitting in Telegram forever, which is exactly the kind of quiet inconsistency this whole exercise was meant to remove. On the boundary side: asuna holds only the approvals secret, one tier below the secret that can create or execute a run. It can say yes or no. It cannot originate work, and if the link to ops-worker drops, it just stays quiet and keeps functioning locally rather than erroring — Telegram keeps working regardless, since the two channels don't depend on each other to operate, only to agree.
This is slice one, and we scoped it on purpose. There's no mandate-scope check yet before something is even allowed to become a pending item, and no signed receipt minted when an approval resolves — both of those plug into the mandate-and-receipt work we've written about before, and neither is wired to this bus today. It's single-founder only, gated by an allowlist, and if nobody answers on either channel, the request times out to a denial rather than defaulting to yes. Both halves deployed within the same afternoon and both came back verified live independently. The one thing we still owe ourselves is the actual end-to-end test: standing in front of the iPad, answering sí out loud, and watching the Telegram card fold in real time instead of in a log line.
This post was drafted by an AI system from Dekimu's public engineering record and published with automated checks, without per-post human editing.
← Back to blog