2026-02-05 08:00:00
Bluetooth headphones are great, but they have one main weakness: they can only get audio streams from a single device at a time. Facts and Circumstances™️ mean that I have to have hard separation of personal and professional workloads, and I frequently find myself doing both at places like coworking spaces.

Often I want to have all of these audio inputs at once:
When I'm in my office at home, I'll usually have all of these on speaker because I'm the only person there. I don't want to disturb people at this coworking space with my notification pings or music.
Turns out a Steam Deck can act as a BlueTooth speaker with no real limit to the number of inputs! Here's how you do it:
This is stupidly useful. It also works with any Linux device, so if you have desktop Linux on any other machines you can also use them as speakers. I really wish this was a native feature of macOS and Windows. It's one of the best features of desktop Linux that nobody knows about.
2026-02-04 08:00:00
I don't know how to properly raise this, but I've gotten at least 100 emails from various Zendesk customers (no discernible pattern, everything from Soundcloud to GitLab Support to the Furbo Pet Camera).
Is Zendesk being hacked?
I'll update the post with more information as it is revealed.
2026-01-27 08:00:00
Hey all! We've got a Discord so you can chat with us about the wild world of object storage and get any help you need. We've also set up Answer Overflow so that you can browse the Q&A from the web.
Today I'm going to discuss how we got there and solved one of the biggest problems with setting up a new community or forum: backfilling existing Q&A data so that the forum doesn't look sad and empty.
All the code I wrote to do this is open source in our glue repo. The rest of this post is a dramatic retelling of the thought process and tradeoffs that were made as a part of implementing, testing, and deploying this pull request.
Ready? Let's begin!
There's a bunch of ways you can think about this problem, but given the current hype zeitgeist and contractual obligations we can frame this as a dataset management problem. Effectively we have a bunch of forum question/answer threads on another site, and we want to migrate the data over to a new home on Discord. This is the standard "square peg to round hole" problem you get with Extract, Transform, Load (ETL) pipelines and AI dataset management (mostly taking your raw data and tokenizing it so that AI models work properly).
So let's think about this from an AI dataset perspective. Our pipeline has three distinct steps:
When thinking about gathering and transforming datasets, it's helpful to start by thinking about the modality of the data you're working with. Our dataset is mostly forum posts, which is structured text. One part of the structure contains HTML rendered by the forum engine. This, the "does this solve my question" flag, and the user ID of the person that posted the reply are the things we care the most about.
I made a bucket for this (in typical recovering former SRE fashion it's named for a completely different project) with snapshots enabled, and then got cracking. Tigris snapshots will let me recover prior state in case I don't like my transformations.
When you are gathering data from one source in particular, one of the first things you need to do is ask permission from the administrator of that service. You don't know if your scraping could cause unexpected load leading to an outage. It's a classic tragedy of the commons problem that I have a lot of personal experience in preventing. When you reach out, let the administrators know the data you want to scrape and the expected load– a lot of the time, they can give you a data dump, and you don't even need to write your scraper. We got approval for this project, so we're good to go!
To get a head start, I adapted an old package of mine to assemble User-Agent strings in such a way that gives administrators information about who is requesting data from their servers along with contact information in case something goes awry. Here's an example User-Agent string:
tigris-gtm-glue (go1.25.5/darwin/arm64; https://tigrisdata.com; +qna-importer) Hostname/hoshimi-miyabi.local
This gives administrators the following information:
os.Args[0], which contains the path the kernel was passed to the executable.This seems like a lot of information, but realistically it's not much more than the average Firefox install attaches to each request:
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:146.0) Gecko/20100101 Firefox/146.0
The main difference is adding the workload hostname purely to help debugging a misbehaving workload. This is a concession that makes each workload less anonymous, however keep in mind that when you are actively scraping data you are being seen as a foreign influence. Conceding more data than you need to is just being nice at that point.
One of the other "good internet citizen" things to do when doing benign scraping is try to reduce the amount of load you cause to the target server. In my case the forum engine is a Rails app (Discourse), which means there's a few properties of Rails that work to my advantage.
Fun fact about Rails: if you append .json to the end of a URL, you typically get a JSON response based on the inputs to the view. For example, consider my profile on Lobsters at https://lobste.rs/~cadey. If you instead head to https://lobste.rs/~cadey.json, you get a JSON view of my profile information. This means that a lot of the process involved gathering a list of URLs with the thread indices we wanted, then constructing the thread URLs with .json slapped on the end to get machine-friendly JSON back.
This made my life so much easier.
Now that we have easy ways to get the data from the forum engine, the next step is to copy it out to Tigris directly after ingesting it. In order to do that I reused some code I made ages ago as a generic data storage layer kinda like Keyv in the node ecosystem. One of the storage backends was a generic object storage backend. I plugged Tigris into it and it worked on the first try. Good enough for me!
Either way: this is the interface I used:
// Interface defines the calls used for storage in a local or remote datastore.
// This can be implemented with an in-memory, on-disk, or in-database storage
// backend.
type Interface interface {
// Delete removes a value from the store by key.
Delete(ctx context.Context, key string) error
// Exists returns nil if the key exists, ErrNotFound if it does not exist.
Exists(ctx context.Context, key string) error
// Get returns the value of a key assuming that value exists and has not expired.
Get(ctx context.Context, key string) ([]byte, error)
// Set puts a value into the store that expires according to its expiry.
Set(ctx context.Context, key string, value []byte) error
// List lists the keys in this keyspace optionally matching by a prefix.
List(ctx context.Context, prefix string) ([]string, error)
}
By itself this isn't the most useful, however the real magic comes with my JSON[T] adaptor type. This uses Go generics to do type-safe operations on Tigris such that you have 90% of what you need for a database replacement. When you do any operations on a JSON[T] adaptor, the following happens:
In the future I hope to extend this to include native facilities for forking, snapshots, and other nice to haves like an in-memory cache to avoid IOPs pressure, but for now this is fine.
As the data was being read from the forum engine, it was saved into Tigris. All future lookups to that data I scraped happened from Tigris, meaning that the upstream server only had to serve the data I needed once instead of having to constantly re-load and re-reference it like the latest batch of abusive scrapers seem to do.
So now I have all the data, I need to do some massaging to comply both with Discord's standards and with some arbitrary limitations we set on ourselves:
In general, this means I needed to take the raw data from the forum engine and streamline it down to this Go type:
type DiscourseQuestion struct {
Title string `json:"title"`
Slug string `json:"slug"`
Posts []DiscoursePost `json:"posts"`
}
type DiscoursePost struct {
Body string `json:"body"`
UserID string `json:"userID"`
Accepted bool `json:"accepted"`
}
In order to make this happen, I ended up using a simple AI agent to do the cleanup. It was prompted to do the following:
I figured this should be good enough so I sent it to my local DGX Spark running GPT-OSS 120b via llama.cpp and manually looked at the output for a few randomly selected threads. The sample was legit, which is good enough for me.
Once that was done I figured it would be better to switch from the locally hosted model to a model in a roughly equivalent weight class (gpt-5-mini). I assumed that the cloud model would be faster and slightly better in terms of its output. This test failed because I have somehow managed to write code that works great with llama.cpp on the Spark but results in errors using OpenAI's production models.
I didn't totally understand what went wrong, but I didn't dig too deep because I knew that the local model would probably work well enough. It ended up taking about 10 minutes to chew through all the data, which was way better than I expected and continues to reaffirm my theory that GPT-OSS 120b is a good enough generic workhorse model, even if it's not the best at coding.
From here things worked, I was able to ingest things and made a test Discord to try things out without potentially getting things indexed. I had my tool test-migrate a thread to the test Discord and got a working result.
To be fair, this worked way better than expected (I added random name generation and as a result our CEO Ovais, became Mr. Quinn Price for that test), but it felt like one thing was missing: avatars. Having everyone in the migrated posts use the generic "no avatar set" avatar certainly would work, but I feel like it would look lazy. Then I remembered that I also have an image generation model running on the Spark: Z-Image Turbo. Just to try it out, I adapted a hacky bit of code I originally wrote on stream while I was learning to use voice coding tools to generate per-user avatars based on the internal user ID.
This worked way better than I expected when I tested how it would look with each avatar attached to their own users.
In order to serve the images, I stored them in the same Tigris bucket, but set ACLs on each object so that they were public, meaning that the private data stayed private, but anyone can view the objects that were explicitly marked public when they were added to Tigris. This let me mix and match the data so that I only had one bucket to worry about. This reduced a lot of cognitive load and I highly suggest that you repeat this pattern should you need this exact adaptor between this exact square peg and round hole combination.
Now that everything was working in development, it was time to see how things would break in production! In order to give the façade that every post was made by a separate user, I used a trick that my friend who wrote Pluralkit (an accessibility tool for a certain kind of neurodivergence) uses: using Discord webhooks to introduce multiple pseudo-users into one channel.
I had never combined forum channels with webhook pseudo-users like this before, but it turned out to be way easier than expected. All I had to do was add the right thread_name parameter when creating a new thread and the thread_id parameter when appending a new message to it. It was really neat and made it pretty easy to associate each thread ingressed from Discourse into its own Discord thread.
Then all that was left was to run the Big Scary Command™ and see what broke. A couple messages were too long (which was easy to fix by simply manually rewriting them, doing the right state layer brain surgery, deleting things on Discord, and re-running the migration tool. However 99.9% of messages were correctly imported on the first try.
I had to double check a few times including the bog-standard wakefulness tests. If you've never gone deep into lucid dreaming before, a wakefulness test is where you do something obviously impossible to confirm that it does not happen, such as trying to put your fingers through your palm. My fingers did not go through my palm. After having someone else confirm that I wasn't hallucinating more than usual I found out that my code did in fact work and as a result you can now search through the archives on community.tigrisdata.com or via the MCP server!
I consider that a massive success.
As someone who has seen many truly helpful answers get forgotten in the endless scroll of chats, I wanted to build a way to get that help in front of users when they need it by making it searchable outside of Discord. Finding AnswerOverflow was pure luck: I happened to know someone who uses it for the support Discord for the Linux distribution I use on my ROG Ally, Bazzite. Thanks, j0rge!
AnswerOverflow also has an MCP server so that your agents can hook into our knowledge base to get the best answers. To find out more about setting it up, take a look at the "MCP Server" button on the Tigris Community page. They've got instructions for most MCP clients on the market. Worst case, configure your client to access this URL:
https://community.tigrisdata.com/mcp
And bam, your agent has access to the wisdom of the ancients.
But none of this is helpful without the actual answers. We were lucky enough to have existing Q&A in another forum to leverage. If you don't have the luxury, you can write your own FAQs and scenarios as a start. All I can say is, thank you to the folks who asked and answered these questions– we're happy to help, and know that you're helping other users by sharing.
Connect with other developers, get help, and share your projects. Search our Q&A archives or ask a new question. Join the Discord.
2026-01-15 08:00:00
I don't like being interrupted when I'm deep in flow working on things. When my flow is interrupted, it can feel like my focus was violently stolen from me and the mental context that was crystalline falls apart into a thousand pieces before it is lost forever. With this in mind, being asked to do a "quick" 5 minute task can actually result in over an hour of getting back up to speed.
This means that I sometimes will agree to do things, go back into flow (because if I get back into flow almost instantly I'm more likely to not lose any context), forget about them, and then look bad as a result. This is not ideal for employment uptime.
When you work at a startup, you don't do your job; you project the perception of doing it and ensure that the people above you are happy with what you are doing. This is a weird fundamental conflict and understanding this at a deep level has caused a lot of strange thoughts about the nature of the late-stage capitalism that we find ourselves in.
However, it's the future and we have tools like Claude Code. As much as I am horrified by the massive abuses the AI industry is doing to the masses with abusive scraping, there are real things that the tools the AI industry can do today. The biggest thing they can do is just implement those "quick requests" because most of them are on the line of:
Nearly 90% of these are in fact things that tools the AI industry has released can do today. I could just open an AI coding agent and tell it to go to town, but we can do better.
Claude Code has custom slash command support. In Claude Code land, slash commands are prompt templates that you can hydrate with arguments. This means you can just describe the normal workflow process and have the agent dutifully go about and get that done for you while you focus on more important things.
Here's what those commands look like in practice:
Please make the following change:
$ARGUMENTS
When you are done, do the following:
- Create a Linear issue for this task.
- Create a branch based on the changes to be made and my github username (eg:
ty/update-readme-not-mention-foo).- Make a commit with the footer
Closes: (linear issue ID)and use the--signoffflag.- Push that branch to GitHub.
- Create a pull request for that branch.
- Make a comment on that pull request mentioning
${CEO_GITHUB_USERNAME}.When all that is done, please reply with a message similar to the following:
> Got it, please review this PR when you can: (link).
So whenever I get a "quick request", I can open a new worktree in something like Conductor, copy that Slack message verbatim, then type in:
/quick-request add a subsection to the README pointing people to the Python repository (link) based on the subsections for Go and JavaScript
From there all I have to do is hit enter and then go back to writing. The agent will dutifully Just Solve The Thing™️ using GLM 4.7 via their coding plan. It's not as good as Anthropic's models, but it works well enough and has a generous rate limit. It's good enough, and good enough is good enough for me.
I realize the fundamental conflict between what I work on with Anubis and this tormentmaxxing workflow, but if these tools are going to exist regardless of what I think is "right", is decently cheap, and is genuinely useful, I may as well take advantage of this while the gravy train lasts.
Remember: think smarter, not harder.
2026-01-11 08:00:00
My coworkers really like AI-powered code review tools and it seems that every time I make a pull request in one of their repos I learn about yet another AI code review SaaS product. Given that there are so many of them, I decided to see how easy it would be to develop my own AI-powered code review bot that targets GitHub repositories. I managed to hack out the core of it in a single afternoon using a model that runs on my desk. I've ended up with a little tool I call reviewbot that takes GitHub pull request information and submits code reviews in response.
reviewbot is powered by a DGX Spark, llama.cpp, and OpenAI's GPT-OSS 120b. The AI model runs on my desk with a machine that pulls less power doing AI inference than my gaming tower pulls running fairly lightweight 3D games. In testing I've found that nearly all runs of reviewbot take less than two minutes, even at a rate of only 60 tokens per second generated by the DGX Spark.
reviewbot is about 350 lines of Go that just feeds pull request information into the context window of the model and provides a few tools for actions like "leave pull request review" and "read contents of file". I'm considering adding other actions like "read messages in thread" or "read contents of issue", but I haven't needed them yet.
To make my life easier, I distribute it as a Docker image that gets run in GitHub Actions whenever a pull review comment includes the magic phrase /reviewbot.
The main reason I made reviewbot is that I couldn't find anything like it that let you specify the combination of:
I'm fairly sure that there are thousands of similar AI-powered tools on the market that I can't find because Google is a broken tool, but this one is mine.
When reviewbot reviews a pull request, it assembles an AI model prompt like this:
Pull request info:
<pr>
<title>Pull request title</title>
<author>GitHub username of pull request author</author>
<body>
Text body of the pull request
</body>
</pr>
Commits:
<commits>
<commit>
<author>Xe</author>
<message>
chore: minor formatting and cleanup fixes
- Format .mcp.json with prettier
- Minor whitespace cleanup
Assisted-by: GLM 4.7 via Claude Code
Reviewbot-request: yes
Signed-off-by: Xe Iaso <[email protected]>
</message>
</commit>
</commits>
Files changed:
<files>
<file>
<name>.mcp.json</name>
<status>modified</status>
<patch>
@@ -3,11 +3,8 @@
"python": {
"type": "stdio",
"command": "go",
- "args": [
- "run",
- "./cmd/python-wasm-mcp"
- ],
+ "args": ["run", "./cmd/python-wasm-mcp"],
"env": {}
}
}
-}
\ No newline at end of file
+}
</patch>
</file>
</files>
Agent information:
<agentInfo>
[contents of AGENTS.d in the repository]
</agentInfo>
The AI model can return one of three results:
submit_review tool that approves the changes with a summary of the changes made to the code.submit_review tool that rejects the changes with a summary of the reason why they're being rejected.The core of reviewbot is the "AI agent loop", or a loop that works like this:
submit_review tool, publish the results and exit.reviewbot is a hack that probably works well enough for me. It has a number of limitations including but not limited to:
When such an innovation as reviewbot comes to pass, people naturally have questions. In order to give you the best reading experience, I asked my friends, patrons, and loved ones for their questions about reviewbot. Here are some answers that may or may not help:
Probably not! This is something I made out of curiosity, not something I made for you to actually use. It was a lot easier to make than I expected and is surprisingly useful for how little effort was put into it.
Nope. Pure chaos. Let it all happen in a glorious way.
How the fuck should I know? I don't even know if chairs exist.
At least half as much I have wanted to use go wish for that. It's just common sense, really.
When the wind can blow all the sand away.
Three times daily or the netherbeast will emerge and doom all of society. We don't really want that to happen so we make sure to feed reviewbot its oatmeal.
At least twelve. Not sure because I ran out of pancakes.
Only if you add that functionality in a pull request. reviewbot can do anything as long as its code is extended to do that thing.
Frankly, you shouldn't.
2026-01-02 08:00:00
TL;DR: 2026 is going to be The Year of The Linux Desktop for me. I haven't booted into Windows in over 3 months on my tower and I'm starting to realize that it's not worth wasting the space for. I plan to unify my three SSDs and turn them all into btrfs drives on Fedora.
I've been merely tolerating Windows 11 for a while but recently it's gotten to the point where it's just absolutely intolerable. Somehow Linux on the desktop has gotten so much better by not even doing anything differently. Microsoft has managed to actively sabotage the desktop experience through years of active disregard and spite against their users. They've managed to take some of their most revolutionary technological innovations (the NT kernel's hybrid design allowing it to restart drivers, NTFS, ReFS, WSL, Hyper-V, etc.) then just shat all over them with start menus made with React Native, control-alt-delete menus that are actually just webviews, and forcing Copilot down everyone's throats to the point that I've accidentally gotten stuck in Copilot in a handheld gaming PC and had to hard reboot the device to get out of it. It's as if the internal teams at Microsoft have had decades of lead time in shooting each other in the head with predictable results.
To be honest, I've had enough. I'm going to go with Fedora on my tower and Bazzite (or SteamOS) on my handhelds.
I think that Linux on the desktop is ready for the masses now, not because it's advanced in a huge leap/bound. It's ready for the masses to use because Windows has gotten so much actively worse that continuing to use it is an active detriment to user experience and stability. Not to mention with the price of ram lately, you need every gigabyte you can get and desktop Linux lets you waste less of it on superfluous bullshit that very few people actually want.
At the very least, when something goes wrong on Linux you have log messages that can let you know what went wrong so you can search for it.