Skip to main content

Posts

Showing posts from 2026

Talking to Copilot like a caveman

  I think that everyone who uses AI recognizes the following pattern; you ask an LLM a simple question and it answers like it's writing a blog post: introduction, context, three examples, a closing summary. Fine for a first read, expensive when you're chaining calls or running an agent loop all day. The trick to avoid this is called "caveman prompting". You tell the model to drop articles, pleasantries and filler, and answer in short, blunt fragments. It sounds silly. But it works up to a point. A first attempt: just say "be concise" Most people's first instinct is a one-line system prompt: Be concise. No fluff. This already gets you a good chunk of the savings. In benchmarks I've seen floating around, a plain "be concise, return structured output" instruction accounts can already give you a nice reduction. It's the cheapest fix and most people stop here, which is reasonable. The caveman approach The caveman skill takes...

Cache stampede: when our cache turned against us

While investigating some performance issues, we ran into an ASP.NET Core API that cached a fairly expensive aggregation query for 60 seconds. Under normal load, that was fine: one request rebuilds the cache, everyone else reads from it. Under peak load, dozens of requests would arrive in that same expiry window, all see a cache miss, and all fire the same expensive query in parallel. The database didn't like that. That was the moment when our caching layer stopped helping and started hurting. A burst of requests comes in at the same time, all miss the cache, and all go hammer the database or the downstream API at once. That's a cache stampede . The cache was supposed to protect our backend, and for a few hundred milliseconds it did the opposite. Why this happens IMemoryCache.GetOrCreate (and its async sibling) looks like it protects you, but it doesn't add any locking on its own. Look at the naive version: public async Task<Report> GetReportAsync(string key) ...

A complex system designed from scratch never works

A few years ago, I worked as an architect on a big mainframe rewrite. I still count it as one of my failures. Not because the technology was wrong, but because I couldn't convince the management team to simplify the approach. Years later, the organization is still struggling to get the new system up and running. I left the project at the time, because I couldn't put my name behind an approach that would take very long and cost a lot of money without a working system to show for it along the way. Gall’s Law That memory keeps coming back to me, because it's a textbook case of Gall's Law playing out in real life. Gall's Law , from John Gall's Systemantics , states it plainly: A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works, and it cannot be patched to make it work. You have to start over with a simple system that works. What does that mean in practice,...

Fixing "Filename too long" errors on Windows with Git

There's a moment when you clone or pull a repository on Windows and Git throws an error like this: error: unable to create file some/very/deeply/nested/path/to/a/file.ts: Filename too long Nothing wrong with your code, nothing wrong with the repo. It's Windows. Why does this happen? Windows has a default path length limitation of 260 characters (the infamous MAX_PATH ). Git operations that create files with a full path longer than that — cloning, checking out, pulling — will fail with this error. Repositories with deeply nested folder structures (think node_modules, or generated code) hit this constantly. The fix: enable long paths in Git Git has a config setting for exactly this: core.longpaths . You have two ways to set it, depending on your rights on the machine. System-wide (requires Administrator privileges): git config --system core.longpaths true User-level (no Administrator required): git config --global core.longpaths true If y...

YARP and Aspire: "https+http scheme is not supported"

Recently I was wiring up a YARP reverse proxy in front of a couple of Aspire-managed services: an API and an Angular frontend. Aspire gives you service discovery for free, so the obvious move is to point your YARP clusters at the logical service names instead of hardcoded URLs. My first attempt looked like this: "Clusters": { "api-cluster": { "Destinations": { "api-destination": { "Address": "https+http://api" } } }, "frontend-cluster": { "Destinations": { "frontend-destination": { "Address": "https+http://angular-frontend" } } } } The https+http:// scheme is the standard Aspire service discovery convention: try HTTPS first, fall back to HTTP. It works fine when you're resolving endpoints through HttpClient . Unfortunately YARP doesn’t like this configuration. After setting it up with these values ...

The role of ActivitySource in OpenTelemetry for .NET

While doing some pair programming to integrate OpenTelemetry tracing to a .NET application, we had a discussion on how to use the ActivitySource . It looks simple. You new one up, give it a name, start an activity, done. The discussion started when we added a second ActivitySource with the exact same name in a different class. This made us wonder: "Are we duplicating traces now? Is this a memory leak? Do we need a singleton?" So we decided to dig deeper. This post is what we learned… What ActivitySource actually is ActivitySource is part of System.Diagnostics , not part of the OpenTelemetry NuGet packages. Microsoft built tracing primitives directly into the BCL, and OpenTelemetry's .NET SDK simply listens to them. This is why you can add distributed tracing to a library without taking a dependency on OpenTelemetry at all. An ActivitySource is a factory for Activity objects, and an Activity is .NET's name for what OpenTelemetry calls a span.(don’t ask m...

Combining Google Stitch with the GitHub Copilot Coding Agent

UI generation and background coding agents are two of the "AI" tools that really changed my way of working. Together, they close a gap that's been annoying me for a while: the coding agent still needs someone to describe what the UI should look like, and that someone is usually me, typing a wall of text into an issue and hoping for the best. Google Stitch generates UI screens (HTML/CSS, Tailwind, Flutter, SwiftUI, whatever…) from a prompt or a sketch. The GitHub Copilot coding agent picks up an issue and produces a pull request in the background, without you sitting in the editor. In this post we look at how to connect the two through MCP, so the coding agent stops guessing at layout, spacing and colors, and starts reading an actual design spec. Here's how to wire it up, and where it still needs a human in the loop. My first approach: screenshots in the issue body My first attempt was to design something in Stitch, paste a screenshot into a GitHub issue, and ...

Dependency-Track: Error occurred decrypting the OSS Index API Token

We run Dependency-Track for vulnerability analysis across our projects. One day, the OSS Index integration stopped working. No API calls, no analysis results, just this in the logs: An error occurred decrypting the OSS Index API Token; Skipping [projectName=Balansen - BatchConsole, vulnAnalysisLevel=PERIODIC_ANALYSIS, projectUuid=14ba5633-58c2-45fe-9f04-e4ab0b28375e, projectVersion=DEV] javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption. BadPaddingException is Java's polite way of saying: "I tried to decrypt this with a key, and it's the wrong key." The API token itself was fine. The key used to decrypt it wasn't. The wrong assumption Our first instinct was to re-enter the OSS Index credentials in the UI and assume a fat-fingered token was the culprit. That didn't help. The error came back on the next analysis run, right after we restarted the container for an unrelated...

Slowly Changing Dimensions in Microsoft Fabric - The no-code way

Dimension tables don't stay still. A customer moves city, a product gets reclassified, a salesperson switches regions. The question is never whether this happens, it's what you do with the old value once it changes. That question has a name: Slowly Changing Dimensions (SCD). If you haven't already, it's worth reading my first post about what SCDs actually are and the full set of types first — this post assumes you already know the difference between Type 1 and Type 2 and want to get straight to implementing Type 2 in Fabric. Until recently, implementing Type 2 in Fabric meant either building a Dataflow Gen2 with a chain of merge steps, or writing a PySpark notebook against Delta tables. Both work. Both also mean you're maintaining custom logic per table, forever. Fabric's Copy job now has SCD Type 2 built in as a write method. No merge statements, no derived columns for surrogate keys, no alter-row logic. You pick a write method from a dropdown. This pos...

Slowly Changing Dimensions – An introduction

Being new to data warehousing, I never heard about the term "Slowly Changing Dimensions" before. It sounded like academic jargon. It isn't. It's one of those concepts that, once it clicks, explains half the weird design decisions you'll see in any reporting database. This post is the explainer I wish existed before I had to learn it the hard way: by breaking a report Facts and dimensions, the short version Data warehouses generally split data into two kinds of tables. Fact tables hold the things that happened: an order, a sale, a support ticket, a click. They're typically just numbers and foreign keys — quantity, amount, a reference to which customer, which product, which date. Dimension tables hold the descriptive context around those facts: who the customer is, what the product is called, which region a salesperson covers. A fact on its own is nearly meaningless. "47 units, customer 1182, product 309" tells you nothing until you jo...

GitHub Copilot SDK Deep Dive: Session Memory

The GitHub Copilot SDK just shipped a new feature: optional memory configuration on session create and resume. Here is what it does, and how it is different from persisted sessions. The wrong mental model first When I heared "session memory" my first thought was "persisted sessions" — the ResumeSessionAsync flow that lets you reload an existing session by ID and continue where you left off. That is not what this is. Persisted sessions are about durability of the conversation itself: close the app, reopen it, pick up the thread. Memory configuration is something different. What memory configuration actually does Memory is a feature of the Copilot runtime that lets the agent read and write facts across turns — a kind of long-running knowledge store that the agent can consult and update during a session. Think of it as the agent's notepad, not the conversation log. The new MemoryConfiguration type exposes a single Enabled flag today. You opt in per se...

Scheduling actions in the GitHub Copilot CLI

The GitHub Copilot CLI keeps getting more capable. One of the newer additions is the ability to schedule prompts, either on a repeating interval or as a one-shot delayed action. Let me walk you through both approaches. Two ways to schedule The /every command schedules a prompt to run repeatedly at a specified interval, while /after schedules a one-shot prompt to run once after a specified delay. Both commands are still experimental. They are only available if you have used the /experimental on slash command, or the --experimental command-line option first. Use the following slash command to enable experimental mode in your session: /experimental on Recurring prompts with /every Use /every when you want Copilot to repeat a task on a cadence during your session. /every 10m Run the test suite and summarize any new failures /every 1h Check for new comments on my open pull requests A number with no suffix is interpreted as minutes — so /every 30 remind me to check for...

Configuring Copilot CLI Isolation via the GitHub Copilot SDK

In the previous post, we walked through local sandboxing in the Copilot CLI: enable it with /sandbox enable , tune filesystem and network rules through the TUI, and your agent's shell execution is isolated by Microsoft MXC. Simple, useful, done. But if you're building with the Copilot SDK, embedding the agent runtime into your own .NET application, you can't type /sandbox enable into a session you're programmatically orchestrating. So the question becomes: how do you get the same isolation guarantees when you own the host? The good news: sandbox support is coming to the SDK as a preview feature. The entry point is Session.Rpc.Options.UpdateAsync , and it lets you push a sandbox configuration into a running session from code. Preview caveat : this API is behind the experimental surface of the SDK. It's real, it works, but the shape may change before it stabilises. Treat it as preview-quality and don't build production contracts on top of it just yet. Wha...

Local sandboxing in the GitHub Copilot CLI

There's a moment in every agentic workflow where you pause and think: wait, what exactly is Copilot allowed to touch right now? For a long time the answer was: pretty much everything under your working directory and whatever shell commands it decides to run to get the job done. That was fine when Copilot was mostly suggesting code. It's a different story when it's running tools, executing scripts, and modifying files on your behalf. As of June 2026, GitHub has an answer: local sandboxing , now in public preview. It doesn't replace good judgment about what you ask Copilot to do, but it does put a real isolation boundary between the agent's tool execution and the rest of your machine. Let’s explore this feature… Why do we need this? The Copilot CLI has evolved significantly since GA. What started as a smart terminal assistant now has Autopilot mode, /plan , fleet parallelism, rubber duck, and a full agentic harness underneath. When you run Copilot in Autopil...