A new release of the Microsoft build of Go including security fixes is now available for download.
For more information about this release and the changes included, see the table below:
Recommender systems are everywhere. Whether in retail, entertainment, social platforms, or embedded into enterprise marketing software, recommender systems are the invisible engine in modern markets, driving efficiency on both sides of the supply-demand equation. Every day they help consumers find their way through millions of options in the digital world to quickly find the products and services they want, while for businesses, they help product, sales and marketing teams to align and match their company's offerings with potential customers.
To build and maintain effective recommender systems, software engineers must manage significant challenges, like technical complexity, privacy, and security. Furthermore, they must ensure these systems remain scalable while delivering on high-quality intelligent recommendations and semantic search. This article looks at the Red Hat AI Product Recommender AI quickstart and walk through how Red Hat OpenShift AI helps engineers tackle these challenges.
Product recommender AI quickstart
AI quickstarts are sample applications that demonstrate how Red Hat AI products, such as OpenShift AI, provide a platform for your AI applications. While not intended as production-ready solutions, they demonstrate how engineers can integrate key OpenShift AI technologies and third-party libraries to build modern AI-enabled applications.
This AI quickstart demonstrates how Red Hat OpenShift AI helps organizations boost online sales and reshape product discovery by implementing these core AI-driven business functions:
Machine learning (ML) models that make accurate product recommendations
Semantic product search capabilities using text and image queries
Automated product review summarization
This series is organized into three parts:
Part 1: AI technology overview and background (this post)
Part 2: Two-Tower recommender model architecture and training
Part 3: AI-generated product review summaries and new user registration
Overview of AI technologies that support the recommender
In this section, we provide an overview of the technologies our AI quickstart is built on, while subsequent sections are reserved for in-depth coverage. Figure 1 shows these technologies.
Figure 1: Product Recommender AI components.
LLMs and machine learning models
Working clockwise from the top left in Figure 1, the AI quickstart relies on advanced LLMs and ML models to perform key functions. Specifically, the AI quickstart uses the four models described in Table 1.
Table 1: Product recommender models.
Model
Function
BAAI/BGE-small-en-v1.5 text embedding model
Converts product descriptions, titles and other text to embeddings (lists of numbers) to enable semantic search.
openai/clip-vit-base-patch32 image embedding model
Embeds product images to enable image-based queries.
Llama 3.1 8B
Generates product review summaries.
Two-tower recommender model
Provides product recommendations.
Engineers must consider many factors when choosing ML models, but the most important ones are the task and type of data the model supports. The AI quickstart uses the first two models in Table 1 on the embedding task for text and image data. These models accept chunks of text and images and produce numeric representations that align with their semantic content. For example, embeddings for two different cell phone models are closer together than embeddings for a cell phone and a blender. The AI quickstart uses this proximity to enable robust search that returns accurate matches despite minor variations in the input. We discuss embeddings in greater detail later in this article.
The AI quickstart uses the Llama 3.1 8B model on the generative task for text inputs. Specifically, the AI quickstart prompts the model to summarize the reviews for each product. Like the first two models in Table 1, the Llama 3.1 model is an LLM (large language model), but its decoder-only architecture is designed to generate tokens sequentially in response to instructions, whereas the first two models are encoder-only models better for creating embeddings.
The final model in our list is the two-tower model, which is another embedding model except that it bears a dual-encoder architecture. This model knows how to represent products and users as embeddings in a coordinated fashion that reflects how well these two entities interact with each other. We will make this idea much clearer in part 2 of this article.
Though engineers don't always need to train ML models from scratch, as is the case with the first three models in Table 1, they nonetheless need to manage their acquisition, storage, metadata, evaluation and runtime use. OpenShift AI provides a complete suite of integrated technologies designed to manage these tasks across various roles within development teams, including data scientists, ML engineers and MLOps specialists. These technologies are integrated into OpenShift to help enterprises use existing infrastructure investments to build AI-enabled applications with centralized security, governance, and resource provisioning.
Our AI quickstart and this article focus only on a subset of these technologies, including workbenches (familiar Jupyter notebooks) that support model acquisition and evaluation, the Red Hat inference server that stands up LLMs as scalable services, Feast for managing product and user features (including embeddings), and the OpenShift AI pipeline server built on Kubeflow Pipelines and Argo Workflows to manage model training.
Workbenches
Workbenches support data scientists and ML engineers by enabling the rapid creation of Jupyter notebooks within the OpenShift cluster. OpenShift offers preconfigured notebook images—including Data Science, TensorFlow, PyTorch, and TrustyAI— to support the full ML lifecycle from model acquisition to prototyping. Administrators can also supplement these with custom images. Part 3 explores how workbenches facilitate moving models from external registries (such as the Red Hat AI repository on Hugging Face) into enterprise-controlled, S3-compatible storage. While direct deployment from OCI-compliant images is possible, workbenches allow engineers to first evaluate and improve model performance with techniques like quantization.
Inference servers
The ML models in Table 1 are static in nature; i.e., they lack the means to generate text, make predictions or create the data representations we require for semantic search. The models only represent the patterns they've learned over their training data as structured collections of billions of numbers. To apply these patterns to unseen data samples and application tasks, we need libraries that can load these numbers into memory and run computations over them.
OpenShift AI achieves this in several ways and, moving to the right in Figure 1, we see that inference servers play a critical role. Inference servers are to machine learning models what database query engines are to static datasets. They respond to user queries to generate answers using the available data. Most importantly, they do this efficiently for thousands of concurrent requests while keeping each query and response isolated from one another and secure from unauthorized access.
A naïve approach that processes one request at a time would lead to long wait times and an inconsistent user experience. This would be like waiting in a long grocery line behind full shopping carts when you only have a few items.
To solve this, OpenShift AI uses continuous batching. Instead of finishing one customer's entire cart before moving to the next, the system processes one item from every customer's cart in a continuous cycle. When paired with accelerators like NVIDIA GPUs or Intel Gaudi, this technique ensures the hardware remains active so users get results faster.
In short, OpenShift AI's inference server, together with the KServe framework, takes otherwise static ML models and turns them into scalable services on an OpenShift cluster.
Feature store
Our AI quickstart's core recommender model as well as its semantic and image search capabilities directly rely on the consistent management of user and product features. Features are data elements like product descriptions and user preferences in our user and product database tables. Because ML models learn only from prior experience or data, the features used during training must match those used later during inference; otherwise, accuracy suffers through a phenomenon known as training-serving skew.
This is often a more serious and likely outcome than system designers initially anticipate. Within most organizations, data constantly evolves in often subtle ways that don't cause applications to break outright but instead introduce subtle errors that go undetected.
Feast is a feature store that works with a number of vector databases, like Postgres in our case, to provide a single-source-of-truth to define and version features, allowing data to evolve to meet the needs of new applications or requirements without breaking existing applications.
Feast also provides a unified Python API for working with feature data through concepts like data sources, views and services. Figure 2 depicts the following usage pattern our AI quickstart uses:
A data source is defined (for example, a parquet file bundled with the application or a live data source that changes at runtime)
A subset of columns is defined on this data source to create a view
A FeatureService is created on this view
The client code sample in Figure 2 shows how, once these API objects are defined, clients retrieve data using a consistent interface.
Figure 2: Sample Feast API usage pattern.
Model training pipeline orchestration
Before we can serve the AI quickstart's core recommender model, we need a framework to manage its training workflow. OpenShift AI provides a flexible approach that covers common and advanced ML training workflows (called pipelines) using Kubeflow Pipelines (KFP) and a workflow engine like Argo Workflows. Pipelines are simply batch jobs we are already familiar with in computer science, except with important updates that adapt their use for machine learning in modern containerized environments. Figure 3 shows the OpenShift AI dashboard view of the pipeline that builds the two-tower recommender model, providing visibility into each run's execution logs and data flow across its training stages.
Figure 3: OpenShift AI dashboard recommender pipeline run.
Engineers can describe pipelines and their components through Python decorators (the approach our quickstart uses and as shown in Figure 4) or Elyra (Figure 5).
Figure 4: Kubeflow Pipeline DSL decorator describing a pipeline component.
Figure 4 shows the Python function signature for load_data_from_feast, the first stage in building the recommender model. Pipeline components are regular Python functions decorated with the Kubeflow Pipeline (KFP) DSL (domain specific language); for example, @dsl.component. KFP uses these tags to free developers from the lower-level details of configuring and deploying their pipeline to focus instead on the pipeline's core training logic. In Figure 4, for example, we see how the engineer has indicated the load_data_from_feast function can use a configured baseline container image with one additional Python package. KFP takes care of creating a container from this image and installing the required dependency when the training job is executed. We will discuss KFP in greater detail in part 2.
Engineers can also explore beyond our AI quickstart to use Elyra, a user interface (UI) driven front-end to KFP that Red Hat has integrated with data science workbenches. Elyra enables engineers and data scientists to drag and drop their Jupyter notebooks onto a blank canvas where they can connect them together to quickly build training pipelines.
Semantic search
Now that we've provided a background on the components of OpenShift AI the Product Recommender AI quickstart uses, let's dive deeper into the AI quickstart's semantic search capabilities, beginning with a quick review of semantic embeddings.
Embeddings primer
To work with text and images, ML models must first convert them to lists of numbers called vectors. For example, the sequence <1.2, 3.1, -0.3> illustrates a vector with three components or numbers. One way to think of these numbers is as geometric points in high-dimensional space. Our sample picks out a unique point in only three dimensions, but imagine taking this idea further to 384 dimensions, the vector size that the BGE-small model generates (Table 1). It's intuitive to see that these additional dimensions may help us capture more information, but for these high-dimensional geometric points to be useful at all, we must set their values so they model our semantic notions of products and users. We need the vectors to form an embedding space.
An embedding space applies these vectors in a coordinated way such that the distance between the vectors for similar words is small but also such that the addition or subtraction of these vectors is meaningful. For example, we would like the vectors that describe our products to support the following arithmetic:
Neural networks learn embedding spaces by processing pairs of input data known to be similar or dissimilar or related in some other way, like question and answer pairs. The network slowly modifies its internal weights until the vectors for related pairs are close together and those for unrelated pairs are farther apart. Modern embedding models (like the BGE-small model) are encoder-only LLMs that can represent fine shades of meaning between similar words and handle polysemy, which occurs when a single word has multiple meanings depending on the context. This process of contrastive learning can be applied to create image embeddings and, as we discuss shortly, our AI quickstart's recommendation model.
Search by text and image
Our AI quickstart consists of a main landing page which displays product recommendations to authenticated users as well as a semantic search capability across all its product catalog using text and image queries. The representation of both queries and product data as embeddings enables text matching that is robust to semantically similar variations in product descriptions, like "thin remote controls" versus "slim remotes." Similarly, the image search capability enabled by the CLIP model in Table 1 can successfully match images of the same object even with different lighting or angles.
Our AI quickstart enables semantic text search and image search by computing and storing embeddings for its product catalog in advance. At runtime, the application generates an embedding for the user's query and uses Feast's API to locate products with similar embeddings.
To be precise, for text queries, the AI quickstart employs what's known as hybrid search, a general technique which combines semantic matching with traditional regular expressions. This results in more intuitive ranking in search results that pushes exact text matches to the top of the list where most users would expect to find them. Hybrid search is also useful in applications that already provide a well-defined database structure for certain types of queries. For example, consider an online clothing retailer that lets users filter search results by age group using a drop-down menu; for example, clothing for children, teens, or adults. If the system already uses a dedicated typed field to store this distinction in its product database, then a simple SQL filter is preferred over a semantic match for this aspect of the user's query.
Two-tower model: Recommendations as a search problem
Given what we've discussed about embeddings, it might seem tempting to apply them to generate product recommendations; for example, we could use our embedding model to represent a user's attributes (such as product category preferences) as a vector and then search for products with similar vectors (using product attributes like their descriptions and categories). This would effectively convert our core recommendation task to a much simpler search problem which we know Feast can already handle.
However, this idea only works when the two vectors are part of the same embedding space; otherwise, the relative geometric positions of the vectors are not meaningful. Imagine trying to meet a friend at a restaurant for lunch using a pair of shared coordinates. If you're using a map with standard latitude and longitude coordinates while your friend is relying on a tourist's map using a simpler 20x20 integer grid centered on the city's airport, you probably will end up in different places. The numbers and distances on the two maps do not relate to each other.
Our two-tower model addresses this problem using a custom dual encoder which builds this shared embedding space.
In part 2 of this series, we will look deeper into how the two-tower model is trained and how OpenShift's KFP integration helps engineers so they don't have to tackle it alone.
In this article, we demonstrate how to apply inline styling to text in TX Text Control using regular expressions defined in JSON format. This approach allows for dynamic and flexible text formatting based on specific patterns.
I feel like I got substantial value out of Claude today, and want to document it. I am at the tail
end of AI adoption, so I don’t expect to say anything particularly useful or novel. However, I am
constantly complaining about the lack of boring AI posts, so it’s only proper if I write one.
At TigerBeetle, we are big on
deterministic simulation testing.
We even use it
to track performance,
to some degree. Still, it is crucial to verify performance numbers on a real cluster in its natural
high-altitude habitat.
To do that, you need to procure six machines in a cloud, get your custom version of tigerbeetle
binary on them, connect cluster’s replicas together and hit them with load. It feels like, quarter
of a century into the third millennium, “run stuff on six machines” should be a problem just a notch
harder than opening a terminal and typing ls, but I personally don’t know how to solve it without
wasting a day. So, I spent a day vibecoding my own square wheel.
The general shape of the problem is that I want to spin a fleet of ephemeral machines with given
specs on demand and run ad-hoc commands in a SIMD fashion on them. I don’t want to manually type
slightly different commands into a six-way terminal split, but I also do want to be able to ssh into
a specific box and poke it around.
Direct manipulation is the most natural API, and it pays to extend it over the network boundary.
Peter’s post is an application of a similar idea to a narrow, mundane task of developing on Mac and
testing on Linux. Peter suggests two scripts:
remote-sync synchronizes a local and remote projects. If you run remote-sync inside ~/p/tb
folder, then ~/p/tb materializes on the remote machine. rsync does the heavy lifting, and the
wrapper script implements DWIM behaviors.
It is typically followed by
remote-run some --command,
which runs command on the remote machine in the matching directory, forwarding output back to you.
So, when I want to test local changes to tigerbeetle on my Linux box, I have roughly the following
shell session:
$ cd ~/p/tb/work$ code . # hack here$ remote-sync$ remote-run ./zig/zig build test
The killer feature is that shell-completion works. I first type the command I want to run, taking
advantage of the fact that local and remote commands are the same, paths and all, then hit ^A and
prepend remote-run (in reality, I have rr alias that combines sync&run).
The big thing here is not the commands per se, but the shift in the mental model. In a traditional
ssh & vim setup, you have to juggle two machines with a separate state, the local one and the remote
one. With remote-sync, the state is the same across the machines, you only choose whether you want
to run commands here or there.
With just two machines, the difference feels academic. But if you want to run your tests across
six machines, the ssh approach fails — you don’t want to re-vim your changes to source files six
times, you really do want to separate the place where the code is edited from the place(s) where the
code is run. This is a general pattern — if you are not sure about a particular aspect of your
design, try increasing the cardinality of the core abstraction from 1 to 2.
The third component, dax library, is pretty mundane — just a JavaScript library for shell
scripting. The notable aspects there are:
JavaScript’s template literals,
which allow implementing command interpolation in a safe by construction way. When processing
$`ls ${paths}`,
a string is never materialized, it’s arrays all the way to the exec syscall (
more on the topic).
JavaScript’s async/await, which makes managing concurrent processes (local or remote) natural:
Additionally, deno specifically
valiantly strives
to impose process-level structured concurrency, ensuring that no processes spawned by the script
outlive the script itself, unless explicitly marked detached — a
sourspot of UNIX.
Combining the three ideas, I now have a deno script, called box, that provides a multiplexed
interface for running ad-hoc code on ad-hoc clusters.
A session looks like this:
# Switch to project with local modifications$ cd ~/p/tb/work$ git status --short M src/lsm/forest.zig# Spin up 3 machines, print their IPs$ box create 3108.129.172.206,52.214.229.222,3.251.67.25$ box list0 108.129.172.2061 52.214.229.2222 3.251.67.25# Move my code to remote machines$ box sync 0,1,2# Run pwd&ls on machine 0; now the code is there:$ box run 0 pwd/home/alpine/p/tb/work$ box run 0 lsCHANGELOG.md LICENSE README.md build.zigdocs/ src/ zig/# Setup dev env and run build on all three machines.$ box run 0,1,2 ./zig/download.shDownloading Zig 0.14.1 release build...Extracting zig-x86_64-linux-0.14.1.tar.xz...Downloading completed (/home/alpine/p/tb/work/zig/zig)!Enjoy!# NB: using local commit hash here (no git _there_).$ box run 0,1,2 \ ./zig/zig build -Drelease -Dgit-commit=$(git rev-parse HEAD)# ?? is replaced by machine id$ box run 0,1,2 \ ./zig-out/bin/tigerbeetle format \ --cluster=0 --replica=?? --replica-count=3 \ 0_??.tigerbeetle2026-01-20 19:30:15.947Z info(io): opening "0_0.tigerbeetle"...# Cleanup machines (they also shutdown themselves after 8 hours)$ box destroy 0,1,2
I like this! Haven’t used in anger yet, but this is something I wanted for a long time, and now I
have it
The problem with implementing above is that I have zero practical experience with modern cloud. I
only created my AWS account today, and just looking at the console interface ignited the urge to
re-read The Castle. Not my cup of pu-erh. But I had a hypothesis that AI should be good at wrangling
baroque cloud API, and it mostly held.
I started with a couple of paragraphs of rough, super high-level description of what I want to get.
Not a specification at all, just a general gesture towards unknown unknowns. Then I asked ChatGPT to
expand those two paragraphs into a more or less complete spec to hand down to an agent for
implementation.
This phase surfaced a bunch of unknowns for me. For example, I wasn’t thinking at all that I somehow
need to identify machines, ChatGPT suggested using random hex numbers, and I realized that I do need
0,1,2 naming scheme to concisely specify batches of machines. While thinking about this, I realized
that sequential numbering scheme also has an advantage that I can’t have two concurrent clusters
running, which is a desirable property for my use-case. If I forgot to shutdown a machine, I’d
rather get an error on trying to re-create a machine with the same name, then to silently avoid the
clash. Similarly, turns out the questions of permissions and network access rules are something to
think about, as well as what region and what image I need.
With the spec document in hand, I turned over to Claude code for actual implementation work. The
first step was to further refine the spec, asking Claude if anything is unclear. There were couple
of interesting clarifications there.
First, the original ChatGPT spec didn’t get what I meant with my “current directory mapping” idea,
that I want to materialize a local ~/p/tb/work as remote ~/p/tb/work, even if ~ are different.
ChatGPT generated an incorrect description and an incorrect example. I manually corrected example,
but wasn’t able to write a concise and correct description. Claude fixed that working from the
example. I feel like I need to internalize this more — for current crop of AI, examples seem to be
far more valuable than rules.
Second, the spec included my desire to auto-shutdown machines once I no longer use them, just to
make sure I don’t forget to turn the lights off when leaving the room. Claude grilled me on what
precisely I want there, and I asked it to DWIM the thing.
The spec ended up being 6KiB of English prose. The final implementation was 14KiB of TypeScript. I
wasn’t keeping the spec and the implementation perfectly in sync, but I think they ended up pretty
close in the end. Which means that prose specifications are somewhat more compact than code, but not
much more compact.
My next step was to try to just one-shot this. Ok, this is embarrassing, and I usually avoid
swearing in this blog, but I just typoed that as “one-shit”, and, well, that is one flavorful
description I won’t be able to improve upon. The result was just not good (more on why later), so I
almost immediately decided to throw it away and start a more incremental approach.
In my previous vibe-post, I
noticed that LLM are good at closing the loop. A variation here is that LLMs are good at producing
results, and not necessarily good code. I am pretty sure that, if I had let the agent to iterate on
the initial script and actually run it against AWS, I would have gotten something working. I
didn’t want to go that way for three reasons:
Spawning VMs takes time, and that significantly reduces the throughput of agentic iteration.
No way I let the agent run with a real AWS account, given that AWS doesn’t have a fool-proof way
to cap costs.
I am fairly confident that this script will be a part of my workflow for at least several years,
so I care more about long-term code maintenance, than immediate result.
And, as I said, the code didn’t feel good, for these specific reasons:
It wasn’t the code that I would have written, it lacked my character, which made it hard for me
to understand it at a glance.
The code lacked any character whatsoever. It could have worked, it wasn’t “naively bad”, like the
first code you write when you are learning programming, but there wasn’t anything good there.
I never know what the code should be up-front. I don’t design solutions, I discover them in the
process of refactoring. Some of my best work was spending a quiet weekend rewriting large
subsystems implemented before me, because, with an implementation at hand, it was possible for
me to see the actual, beautiful core of what needs to be done. With a slop-dump, I just don’t
get to even see what could be wrong.
In particular, while you are working the code (as in “wrought iron”), you often go back to
requirements and change them. Remember that ambiguity of my request to “shut down idle cluster”?
Claude tried to DWIM and created some horrific mess of bash scripts, timestamp files, PAM policy
and systemd units. But the right answer there was “lets maybe not have that feature?” (in
contrast, simply shutting the machine down after 8 hours is a one-liner).
The incremental approach worked much better, Claude is good at filling-in the blanks. The very first
thing I did for box-v2 was manually typing-in:
Then I asked Claude to complete the CLIParse function, and I was happy with the result. Note
Show, Don’t Tell
I am not asking Claude to avoid throwing an exception and fail fast instead. I just give fatal
function, and it code-completes the rest.
I can’t say that the code insideCLIParse is top-notch. I’d probably written something more
spartan. But the important part is that, at this level, I don’t care. The abstraction for parsing
CLI arguments feel right to me, and the details I can always fix later. This is how this overall
vibe-coding session transpired — I was providing structure, Claude was painting by the numbers.
In particular, with that CLI parsing structure in place, Claude had little problem adding new
subcommands and new arguments in a satisfactory way. The only snag was that, when I asked to add an
optional path to sync, it went with string | null, while I strongly prefer string | undefined.
Obviously, its better to pick your null in JavaScript and stick with it. The fact that undefineed
is unavoidable predetermines the winner. Given that the argument was added as an incremental small
change, course-correcting was trivial.
The null vs undefined issue perhaps illustrates my complaint about the code lacking character.
| null is the default non-choice. | undefined is an insight, which I personally learned from VS
Code LSP implementation.
The hand-written skeleton/vibe-coded guts worked not only for the CLI. I wrote
and then asked Claude to write the body of a particular function according to the SPEC.md.
Unlike with the CLI, Claude wasn’t able to follow this pattern itself. With one example it’s not obvious,
but the overall structure is that instanceXXX is the AWS-level operation on a single box, and
mainXXX is the CLI-level control flow that deals with looping and parallelism. When I asked Claude
to implement box run, without myself doing the main / instance split, Claude failed to
noticed it and needed a course correction.
I want to be careful — I can’t vouch for correctness and especially completeness of the above
snippet. However, given that the nature of the problem is such that I can just run the code and see
the result, I am fine with it. If I were writing this myself, trial-and-error would totally be my
approach as well.
Then there’s synthesis — with several instance commands implemented, I noticed that many started
with querying AWS to resolve symbolic machine name, like “1”, to the AWS name/IP. At that point I
realized that resolving symbolic names is a fundamental part of the problem, and that it should only
happen once, which resulting in the following refactored shape of the code:
Claude was ok with extracting the logic, but messed up the overall code layout, so the final code
motions were on me. “Context” arguments go first, not last, common prefix is more valuable than
common suffix because of visual alignment.
The original “one-shotted” implementation also didn’t do up-front querying. This is an example of a
shape of a problem I only discover when working with code closely.
Of course, the script didn’t work perfectly the first time and we needed quite a few iterations on
the real machines both to fix coding bugs, as well gaps in the spec. That was an interesting
experience of speed-running rookie mistakes. Claude made naive bugs, but was also good at fixing
them.
For example, when I first tried to box ssh after box create, I got an error. Pasting it into
Claude immediately showed the problem. Originally, the code was doing
aws ec2 wait instance-running
and not
aws ec2 wait instance-status-ok.
The former checks if instance is logically created, the latter waits until the OS is booted. It
makes sense that these two exist, and the difference is clear (and its also clear that OS booted !=
SSH demon started). Claude’s value here is in providing specific names for the concepts I
already know to exist.
Another fun one was about the disk. I noticed that, while the instance had an SSD, it wasn’t
actually used. I asked Claude to mount it as home, but that didn’t work. Claude immediately asked me
to run
$ box run 0 cat /var/some/unintuitive/long/path.log
and that log immediately showed
the problem. This is remarkable! 50% of my typical Linux debugging day is wasted not knowing that a
useful log exists, and the other 50% is for searching for the log I know should exist somewhere.
After the fix, I lost the ability to SSH. Pasting the error immediately gave the answer — by
mounting over /home, we were overwriting ssh keys configured prior.
There were couple of more iterations like that. Rookie mistakes were made, but they were debugged
and fixed much faster than my personal knowledge allows (and again, I feel that is trivia
knowledge, rather than deep reusable knowledge, so I am happy to delegate it!).
It worked satisfactorily in the end, and, what’s more, I am happy to maintain the code, at least to
the extent that I personally need it. Kinda hard to measure productivity boost here, but, given just
the sheer number of CLI flags required to make this work, I am pretty confident that time was saved,
even factoring the writing of the present article!
I’ve recently read The Art of Doing Science and Engineering by Hamming (of distance and code), and
one story stuck with me:
A psychologist friend at Bell Telephone Laboratories once built a machine with about 12 switches and
a red and a green light. You set the switches, pushed a button, and either you got a red or a green
light. After the first person tried it 20 times they wrote a theory of how to make the green light
come on. The theory was given to the next victim and they had their 20 tries and wrote their theory,
and so on endlessly. The stated purpose of the test was to study how theories evolved.
But my friend, being the kind of person he was, had connected the lights to a random source! One day
he observed to me that no person in all the tests (and they were all high-class Bell Telephone
Laboratories scientists) ever said there was no message. I promptly observed to him that not one of
them was either a statistician or an information theorist, the two classes of people who are
intimately familiar with randomness. A check revealed I was right!
A review of five non-Copilot AI coding assistants available in the Visual Studio Marketplace that support Visual Studio 2026 and exceed 100,000 installs.