Part 3 of 8: Durable AI for InterlinedList
In Part 2 I talked myself into a durable worker sitting next to Vercel, reusing the lib/ code the app already trusts. That was the plumbing. Now for the first feature I’d actually run through it, a Generate button, and the two things I won’t ship it without.
One: it has to survive a timeout. Two: it has to ask before it writes.
I keep calling those hard requirements because they are. They’re the reason this design points at a durable workflow instead of another POST handler. And to be clear up front, since AI assistance in the product is still marked Coming Soon: none of this exists yet. I’m brainstorming out loud before I hand pieces of it to Claude.
Picture the obvious version. A route that takes a prompt, calls a model, writes the result. Works great in the demo. Then two things happen.
First, someone asks for a long document. Sixteen thousand tokens of generated Markdown streaming back through a serverless function that has a hard duration ceiling. The function dies mid-stream. The user gets nothing, and depending on where it died, maybe a half-written doc. That’s not a fluke. Any generation big enough to be worth doing is big enough to hit the ceiling.
Second, and this is the one that scares me, the model writes straight into your data. For a plain message, fine, you can delete it. For a List schema, that’s a slow-moving disaster. A list’s schema is a set of ListProperty rows parsed from the DSL, and once rows of data hang off those properties, unwinding a wrong guess gets expensive fast. An AI that invents seven columns and commits them before you’ve looked is a terrible first impression. Undoing a bad schema is the wrong goal. You want to never have written it in the first place.
So the design has to fix both: don’t let a long job die in a function, and don’t write anything the user hasn’t confirmed.
The split I keep coming back to is two endpoints, and the difference between them carries the whole safety story.
/suggest returns a validated artifact and writes nothing. /generate writes.
/suggest is the default path. It produces the thing (a doc draft, a list schema, a set of rows), validates it, and hands it back to the UI for a human to look at. Nothing touches the database. /generate is the second half, and it only runs after a person confirms.
In the durable version, this isn’t two disconnected calls. It’s one workflow that pauses in the middle. The route starts a workflow and returns an id. The workflow generates the artifact, then blocks on a Signal (Temporal’s native “wait for an external event”) until a confirm arrives. Long documents don’t block a function while this happens; the UI polls the workflow and reads progress through a Query. The confirm route (/generate/[workflowId]/confirm) sends the signal, the workflow wakes up, and then it writes.
// Sketch of the workflow body (not shipped, this is the design I'd hand off).
export async function generateArtifactWorkflow(input: GenerateInput) {
// 1. Draft on a cheap tier. Result is memoized by Temporal.
const artifact = await generateArtifactActivity(input); // Haiku/Flash draft
const validated = await validateArtifactActivity(artifact); // untrusted, checked
// 2. Pause here. No function held open. No tokens spent waiting.
setHandler(confirmSignal, (decision) => { confirmation = decision; });
await condition(() => confirmation !== undefined);
if (confirmation === "reject") return { status: "discarded" };
// 3. Only now do we write, reusing the same lib/ writers the app trusts.
return await writeArtifactActivity(validated, input.userId);
}
The part I like: while that workflow is parked on the signal, it costs nothing. No held-open request, no tokens burning, no worker thread stuck. Temporal parks it and moves on. You cannot do that inside a serverless handler, and it’s exactly the behavior a confirm gate needs.
The flagship case, the one I’d build the whole pattern around, is “build me a list from a sentence.”
You type “a reading list with title, author, status, and a rating out of five.” The workflow generates a DSL schema, not rows. It renders that schema for you to confirm. You look at it, maybe you fix the rating field, you say yes. Only then does it batch-generate rows to populate the list.
Two phases, and the confirm barrier sits between them for a reason: no row-generation tokens get spent until the schema is confirmed. If the model guessed the schema wrong, you caught it before paying to fill fifty rows into the wrong shape. So confirm-before-spend does double duty here, better UX and a cost control at once, which is the same thread running through this whole series. The cheapest token is the one you never send.
And I’m not inventing new validation to do this. The DSL already has the pieces. validateDSLSchema and parseDSLSchema both live in lib/lists/dsl-parser.ts today, and they’re what turns DSL into ListProperty rows for real, human-authored lists right now. The generated schema goes through the exact same door. Then lib/materialize/build-list.ts builds the actual list. The AI path reuses the trusted path; it doesn’t get a shortcut around it. (Lists are a Subscriber feature, so this whole flow lives behind that gate. The workflow checks it before writing, same as every other list-creating action.)
One mental flip makes the rest of the design fall out easily. Model output is untrusted input. Treat it exactly like a request body from a stranger.
You’d never take a JSON blob off the wire and write it to your database without validating the envelope shape, checking the types, and confirming the user is allowed to do the thing. A model’s output gets the identical treatment: envelope shape first, then DSL validation, then per-row validation, then ownership and subscription gating. Only something that survives all four gates gets written. If the model hallucinates a field type the DSL doesn’t support, validation rejects it the way it’d reject any malformed request. The model doesn’t get trusted just because it’s ours.
If there’s one working-with-an-LLM lesson in this post, it’s this: design the contract, delegate the wiring.
There’s a trust boundary running through this feature, and I want to own every inch of it myself. Three pieces I write by hand: the typed artifact envelope (what a generated doc/list/schema/row is shaped like), the validators (envelope, then DSL, then per-row, then gating), and the ownership and subscription checks. That’s where a subtle mistake means a wrong schema gets written or a free user slips past a gate. I don’t delegate that. A plausible-looking version isn’t good enough. I want to have written it.
Everything around that contract is the kind of thing I’d hand to Claude. The workflow body. The signal handler and the condition wait. The polling glue and the Query that streams progress. The confirm route. It’s durable-execution boilerplate: Claude writes it well, and it’s fast to verify against a contract I already defined.
The hand-off is concrete. I give Claude the typed artifact schema and one rule stated plainly: “validate model output like a request body (envelope, then DSL, then per-row, then gating) and never write it through raw. Here are the validators; call them, don’t reimplement them.” Then I let it build the durable plumbing around that. I own the trust boundary, Claude owns the machinery that carries data across it, and the review stays easy because I already know where the sharp edge is. I made sure it’s mine.
A Generate button that can’t die mid-job and won’t write behind your back is the piece I want first. Next in Part 4, I’ll turn to the cron that already double-posts in production today, and the content calendar I actually want built on top of a scheduler that doesn’t.
Adron brainstorming and working on InterlinedList.
Erik Darling here. Suffering. Suffering in this massive heat wave. Don’t like it. Don’t enjoy it. I mean, the kind of weather… I don’t know. Where’s it always cold? I’m gonna move there. What’s Iceland like this time of year? I don’t know. Maybe Brent had the right idea in 2020. Move to Iceland. It’s never hot.
I mean, you know. I think every beer costs like $30 because they have to helicopter it in from somewhere, but it’s worth it. You never have to deal with heat. Occasionally eat shark that smells like stale urine, I guess. I don’t know. Anyway, let’s learn some more T-SQL. This is the wrong title. Screw that. It’s too hot to change it. In today’s video, we’re gonna talk about performance tuning queries that have to compare the date differences between two columns. There’s one very obvious thing that we can do in the form of the computed column, but then there are other things that we can do depending on our indexing.
We can write into the query. Again, coming back to the idea that sometimes we can align our indexes in our computed columns to our queries. And other times, we can better align our queries to our indexes. If you would like to purchase the full course, where I talk about absolute accuracy, and I talk about the data-driven, absolutely everything in great detail, and you have access to the scripts and the query plans and all the other stuff that we do in here. You can purchase it for $100 off down in the video description. It’s a great time. You can have the whole family sit and watch it, right? Put it on at dinner. Better than leave it to beaver, I think. You can also hire me for consulting. You can become a supporting member of the channel if you would like to part with $4 a month to say thank you for all the hard work that I do here. Ask me office hours questions. That is, of course, free.
So, you know, it makes me feel good. And, of course, please do like, subscribe, tell your friends. Get your children involved early. It’s never too early to start learning about databases. Ruin your childhood. If you are in the market for free, absolutely, totally, no strings attached free SQL Server performance monitoring, you can download the performance monitoring tool that I make. It’s on GitHub. You can read through everything. Down in the video description, you will find the appropriate linkage to get that and grab it. It’s a great time, and it’s only getting better. I can’t make it more free than it is because it is entirely free, but I can make it better. I can add more value to free somehow.
But now, let’s continue suffering. In this dismal weather, let’s talk about SQL Server. So, in the last video where I talked a little bit about precision and date and stuff, this one, we’re going to talk about performance a little bit more. So, we’ve got these two indexes already, right? We’ve got one on the post table in the Stack Overflow 2013 database, one on last activity date creation date, one on creation date last activity date. And a problem that we identified was that whenever we want to date these two columns, SQL Server has no choice but to…
to scan stuff, right? And that… this poses an issue for us as performance tuners because sometimes a scan really isn’t… really isn’t all that good. So, we have to scan our non-clustered index, right? It chose the index on creation date last activity date. Why? It doesn’t matter. It would have had to scan either one, right? And there’s no… there’s no difference. There’s no hidden seek plan here that would have helped SQL Server along. Now, flipping that around a little bit, right? Let’s say…
let’s say that we wanted to try to write this in a more sargable way. I don’t know. How do you describe Rob Farley? Is he a friend? Is he a foe? All I know is that he’s a magician, and you can’t trust magicians, but he is a pretty smart guy. And a long time ago, he wrote a post about sargability and called it, like, Look Both Ways, or something. And Rob had some very good points in that post.
And so, I’m going to try to expand a little bit on that in a slightly different way, because I’m not… addition. I’ll never hassle you with card tricks, but maybe some people are into that. So like if we tried to write this query in this way, so like say we wanted to remove the date diff function from the two column setup, and we said, let’s date add 10 years to one of these columns, maybe SQL Server could take advantage of an index on the other column, because we’re like saying, hey, SQL Server, you can see, you can do all the math on this one column, and then maybe you can seek to the data in this other column, because there’s no function wrapping around this column now. But SQL Server does not do that, right? SQL Server still completely scans this index on creation date, last activity date. And if we try to add in a force seek hint to say, hey, SQL Server, perhaps you would like to try seeking here instead, we get an error that the query processor could not produce a plan of that variety.
And so we are stuck. We were stuck scanning this. Now, one way that you can get around this is you can choose to write your query in a slightly different way and give SQL Server some boundaries, right? So like the whole point here is that we’re looking for places where there are a 10-year difference between creation date and last activity date.
So the first thing that we might do is try to find a maximum value to start with, right? That’s what we’re doing in here, right? We’re saying, give me the max. And this works in either direction, going max or minimum. So we’re going to say where creation date is less than this whole date add construct in here, where we find the max last activity date. And then we will do our final calculation in here like this, right? And this basically mimics the last query, except it gives us a place to seek into, right? And we can see that we do sort of find some stuff in here, right?
We find the max last activity date in this portion of the plan. And then we seek into one of our indexes here for the rows that we care about, right? So we get down to that one row max, and then we return stuff out. And this is a market performance improvement. It’s a little hard to tell because the last query just said 563 milliseconds. And it was very easy to see.
For this query, we have to go and find the max last activity date in this portion of the plan. And then we go into the query time stats to see this 69 finished in 69 milliseconds. So this was a pretty good strategy for this one. And you can even flip that if you need to find like mins too, right? So if we wanted to flip the order of this, and like rather than saying where creation date is less than or equal to subtracting nine years from the max, and then the last activity date is greater than or equal to adding 10 years to creation date, we could flip that to do a min on creation date and last activity date and kind of do that in here.
In this way. But my memory serves, this is basically the same plan. But yeah, so but well, actually, I guess this one is 54 milliseconds, but well, that may be 55. Let’s see. Let’s see what query time stats tells us. Ah, 56 milliseconds. So not so I would consider that 13 milliseconds, maybe some noise. Maybe even if that’s consistent, is 13 milliseconds worth all that flipping around? I don’t know. If you’re having the robots do it, probably it’s nothing happens. Anyway, your company’s paying for it. So again, coming back to the sort of ivory tower way of looking at things, store data, the way you query it, query data, the way you store it, otherwise, you’re gonna have to write some pretty weird queries to find performance satisfaction with things, right? At one point, Microsoft did make an attempt at this sort of thing. They added that date.
Date correlation optimization setting around 2005. But it never really took off. And the basic idea was that if you had two date time columns, and one of them happens to be unique and has a unique constraint or index on it, which I know you people, right? And you obey all the ANSI settings rules applicable for filtered indexes, computed columns, index views, and you create a foreign key between your unique date time column and your non-unique date time.
Then the optimizer would be able to figure some additional stuff out when you’re joining two tables together. So if you wrote a query like this that has all that stuff applied to it, SQL Server would turn it into a query that looks like this, right? It would add this date correlation stuff to it.
And it would say, oh, well, not only do I look this way, but I’ll also look this way. But the problem is that it did that by creating an indexed view for you in the background. And so it’s not terribly surprising why this didn’t go very far and get much traction with the general public. I think Fabiano Emerim is the only person who ever saw, like, really blog about it. But I guess the obvious thing, and this is something that we’ve talked about in many other videos, would be just to simply store data the way that you’re querying it, right? And just assuming that we don’t care so much about all the precision stuff that I talked about in other places, we would just create a computed column. And notice that computed column is just a column that does not have to be persisted in order for us to index it. And because all it produces is an integer, maybe, probably an integer, date diff result, it is deterministic out of the box. We don’t have to do any weird entangling of things in order to make this deterministic. And then all of a sudden, our queries magically pick that up, right? And we just say, hey, I know who you are, right? Unfortunately, I don’t think either of the fancier queries that we wrote pick up on that computed column, but maybe. Let’s just go and have a look-see. I don’t think it happens, but yeah, there we go. Yep, they just use the regular indexes there, which makes sense because the expressions that are in use here are certainly not anywhere near the expression that was used in our computed column, right? It would have to be far more precise than that. But our query picks up on it down here, uses our computed column. We don’t have to worry about the data we care about, and all is generally much better. All right. I hope you enjoyed yourselves. I hope you learned something. This is the Thursday video, so I will see you next Tuesday for office hours. And again, if you want to purchase the full course material that all of this stuff stems from, the link is down in the video below with a coupon for 100 bucks off. All right. Thank you for watching.
If this is the kind of SQL Server stuff you love learning about, you’ll love my training. Blog readers get 25% off the Everything Bundle — over 100 hours of performance tuning content. Need hands-on help? I offer consulting engagements from targeted investigations to ongoing retainers. Want a quick sanity check before committing to a full engagement? Schedule a call — no commitment required.
The post Learn T-SQL With Erik: Performance Solutions For DATEDIFF Queries appeared first on Darling Data.
The Copilot runtime supports rewinding conversation history and tracked file changes. SDKs can now opt into file-change tracking via a new enableFileChangeTracking session option, and then use rewind to restore the session to an earlier checkpoint. (#2321)
// TypeScript
const session = await client.startSession({ enableFileChangeTracking: true });
const rewindPoints = await session.rpc.session.listRewindPoints();
await session.rpc.session.rewind({ rewindPointId: rewindPoints[0].id });// C#
var session = await client.StartSessionAsync(new SessionOptions { EnableFileChangeTracking = true });
var points = await session.Rpc.Session.ListRewindPointsAsync();
await session.Rpc.Session.RewindAsync(new RewindParams { RewindPointId = points[0].Id });# Python
session = await client.start_session(enable_file_change_tracking=True)
points = await session.rpc.session.list_rewind_points()
await session.rpc.session.rewind(rewind_point_id=points[0].id)// Go
session, _ := client.StartSession(ctx, &sdk.SessionOptions{EnableFileChangeTracking: true})
points, _ := session.RPC.Session.ListRewindPoints(ctx)
session.RPC.Session.Rewind(ctx, &sdk.RewindParams{RewindPointId: points[0].Id})// Java
SessionOptions options = new SessionOptions().setEnableFileChangeTracking(true);
CopilotSession session = client.startSession(options).get();
List<RewindPoint> points = session.getRpc().getSession().listRewindPoints().get();
session.getRpc().getSession().rewind(new RewindParams().setRewindPointId(points.get(0).getId())).get();// Rust
let session = client.start_session(SessionOptions { enable_file_change_tracking: Some(true), ..Default::default() }).await?;
let points = session.rpc().session().list_rewind_points().await?;
session.rpc().session().rewind(&RewindParams { rewind_point_id: points[0].id.clone() }).await?;The Java SDK now supports loading the Copilot runtime as a native library (via JNA) directly in-process on Linux x64, eliminating the need for a separate CLI child process. This mirrors the in-process mode already available in .NET and Rust. The feature is marked @CopilotExperimental. (#2301)
To use it, add the native runtime classifier JAR to your Maven dependencies and configure the connection:
<dependency>
<groupId>com.github</groupId>
<artifactId>copilot-sdk-java-runtime</artifactId>
<version>${copilot.version}</version>
<classifier>linux-x64</classifier>
</dependency>CopilotClientOptions options = new CopilotClientOptions()
.setConnection(RuntimeConnection.forInProcess());
CopilotClient client = new CopilotClient(options);
client.start().get();JsonValue, ctx.agent() forwards reasoningEffort and contextTier, and a factory body can no longer start a second top-level run (#2309)Generated by Release Changelog Generator · sonnet46 28.7 AIC · ⌖ 7.73 AIC · ⊞ 8.1K
There’s been a lot of confusion and panic this week about “huge fines”, “drastic measures” and “sweeping new AI rules” in the EU. In reality, it’s a lot more narrow — and a lot more sensible. And mostly it’s about making AI more obvious when it actually needs to be obvious — especially for AI-generated content.
Starting from Aug 2, 2026, AI labelling is a legal requirement for any company that serves EU citizens. And similar to European Accessibility Act, it’s not limited to EU companies. It affects any company worldwide with EU operations as long as their AI output is used by people in the EU. Let’s see what exactly it means for us.

The goal of AI labelling is to help everyone exposed to AI content to recognize, in a clear and distinguishable way, that the content has been artificially generated or manipulated.
According to Article 50(4) of the AI Act, AI labelling applies to:
Both providers (who build or supply the AI system) and deployers (who use it) carry legal obligations. Similar to GDPR and EAA, a company doesn’t escape Article 50 just because it licensed an external AI tool from a third party.
However, it doesn’t mean that all AI-generated content must be explicitly labelled.

Beyond the use cases above, pretty much everything else — the vast majority of AI-assisted work — simply isn’t covered by new transparency rules. Most notably, the disclosure obligation does not apply where the AI-generated text has been reviewed and edited by a human, with a named person or entity taking editorial responsibility for it.
Some confusion circles around what exactly “public interest” means, where it starts and where it ends. On its own, it refers to health, safety, environment, economy, finances, politics, science, or culture. If AI-generated product claims touch upon them, the disclosure rule applies.
Some law firms recommend labelling realistic AI-generated illustrations or photos as a precaution for advertising, marketing and other commercial content. AI-generated product illustrations, photos, or posters do need a disclosure, as long as they resemble a real person, place, object, or event.


But at which point does edited AI content stop being AI content? When a form is pre-filled with AI, but then a user edits it, is it still AI? EU Commission’s guidance is a little fuzzy. Small assistive edits — spellcheck, grammar, formatting, cropping, colour correction, and AI-generated translation — don’t count as AI generation.
AI-generated summaries, composite imagery, substantive rewrites, or adding and removing elements from a photo are considered AI generation. In practice, fine-tuning a sentence a person wrote is fine, but generating the sentence on its own requires a disclosure.
“A human skimmed it before publishing” doesn’t qualify as editorial review. The Commission is explicit that it needs to be substantive, with a named person responsible for the editorial control.
In other words, the fine line lies between intentional manual intervention and automated generation. The latter always has to be disclosed (exception: closed B2B environments).

As part of the Code of Practice, the European Commission has published an EU AI icon set. It’s a specific “AI” mark (similar to the AI label in Carbon Design System) — not the generic ✨ sparkle that many products use to signal AI. The signal must be “clear and distinguishable”.
The sparkle might be too ambiguous to signal AI clearly. Mostly because it’s often used to mean “AI-powered feature”, rather than “this specific content was generated by AI”. That’s the kind of signal EU guidelines are trying to rule out.
![]()
The Commission is explicit: using an icon “does not establish legal compliance by itself.” A barely visible icon, a note buried in the footer, or a label that flashes for a second are all not compliant.
The icon should be clearly visible, with a plain language label and accessible to assistive technologies. A safe bet is to pair any icon with plain text (“AI-generated”) — and it needs to persist when being reshared or downloaded.
In fact, the EU Commission also published Code of Practice on marking and labelling of AI content.
It Isn’t Just EUIt might feel like a yet another regulation coming from the EU, but in reality there are plenty of other similar regulations that emerged recently worldwide:
![]()
All of these are signs of upcoming AI regulation that looks more like a pattern, rather than a coincidence. So if you’re shipping anything AI this year, it’s probably a good idea to have a conversation about what exactly is going to be AI-labelled, and what not.
Wrapping UpOne final note is that new EU AI transparency rules are much broader than US laws on AI disclosure, where certain state laws require disclosures for synthetic human performers, political advertising or specific AI applications.
None of this really deserves panic or confusion. It’s about a fairly simple idea that has been emerging worldwide at almost the same time:
When AI content could easily be mistaken for human content, creators must say so — in a way that is clear, obvious, and unambiguous. And parts of the UI that are AI-generated must be disclosed as such.
If anything, it will help people distinguish between AI slop and not AI — and everybody can only benefit from that.
Meet “Design Patterns For AI Interfaces”Meet Design Patterns For AI Interfaces, Vitaly's new video course with practical examples from real-life products — with a live UX training happening soon. Jump to a free preview.
Meet Design Patterns For AI Interfaces, Vitaly’s video course on interface design & UX.
30 video lessons (10h) + Live UX Training.
100 days money-back-guarantee.
30 video lessons (10h). Updated yearly.
Also available as a UX Bundle with 3 video courses.