<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Temporal | Marcin Wylot, PhD</title>
    <link>https://mwylot.net/tags/temporal/</link>
      <atom:link href="https://mwylot.net/tags/temporal/index.xml" rel="self" type="application/rss+xml" />
    <description>Temporal</description>
    <generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Thu, 09 Jul 2026 00:00:00 +0000</lastBuildDate>
    <image>
      <url>https://mwylot.net/media/logo_hu_e1fbd36c83cd87fc.png</url>
      <title>Temporal</title>
      <link>https://mwylot.net/tags/temporal/</link>
    </image>
    
    <item>
      <title>Incremental Map-Reduce LLM Pipeline with Idempotent State and Prompt-Injection Defense</title>
      <link>https://mwylot.net/portfolio/incremental-mapreduce-insight-pipeline/</link>
      <pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://mwylot.net/portfolio/incremental-mapreduce-insight-pipeline/</guid>
      <description>&lt;p&gt;For the same leadership-development AI platform I built the production infrastructure for (
), I designed and built the insight-synthesis layer — the pipeline that reads each user&amp;rsquo;s entire conversation history and distills it into a small set of durable, personalized insights that evolve as they keep using the product. The system is an incremental map-reduce over an LLM: a MAP stage extracts a structured signal per conversation, a REDUCE stage folds those signals into insights against a bounded, durable profile of the user&amp;rsquo;s history. Built on Temporal for reliability, it re-processes only what changed (keeping per-run LLM cost proportional to new activity rather than total history), treats every LLM output as untrusted input, and is idempotent under retries — no false success, no double-counted history, no silently dropped conversations.&lt;/p&gt;
&lt;h3 id=&#34;the-challenge&#34;&gt;The Challenge&lt;/h3&gt;
&lt;p&gt;The platform runs a Socratic AI companion whose value compounds over time — the more a user reflects, the more the product should understand their evolving practice. Turning that accumulated history into trustworthy, longitudinal insight raised a set of problems that single-shot LLM calls don&amp;rsquo;t have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Longitudinal, not per-session&lt;/strong&gt;: insights must reflect a user&amp;rsquo;s whole arc — dozens to hundreds of conversations — not one chat&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unbounded, ever-growing history&lt;/strong&gt;: a user&amp;rsquo;s transcript only grows, so naively re-reading everything on each run is cost-prohibitive at a per-user, minutes-level cadence&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost discipline&lt;/strong&gt;: only conversations that actually changed should ever hit the LLM again&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Correctness under a distributed retry engine&lt;/strong&gt;: a retried or duplicated run must not double-count history, advance state past unprocessed data, or persist a partial result as &amp;ldquo;done&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM output is untrusted&lt;/strong&gt;: extracted content is &lt;em&gt;persisted&lt;/em&gt; and fed back into later prompts — out-of-vocabulary or injection-poisoned output must never corrupt durable state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt injection&lt;/strong&gt;: conversation text is fully user-controlled and flows directly into extraction prompts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never truncate&lt;/strong&gt;: an arbitrarily long conversation must be fully processed, not clipped to fit a context window&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Personalized, evolving insights synthesized from a user&amp;rsquo;s complete conversation history&lt;/li&gt;
&lt;li&gt;Cost that scales with &lt;em&gt;new&lt;/em&gt; activity, not with total history size&lt;/li&gt;
&lt;li&gt;Provable correctness under Temporal&amp;rsquo;s at-least-once activity execution&lt;/li&gt;
&lt;li&gt;A hardened trust boundary around every LLM call whose output touches persisted state&lt;/li&gt;
&lt;li&gt;Full, lossless processing of long conversations&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;solution-architecture&#34;&gt;Solution Architecture&lt;/h3&gt;
&lt;p&gt;I designed a two-stage map-reduce driven by a completion poller, with a durable per-user profile that lets REDUCE fold new activity forward instead of re-reading history:&lt;/p&gt;
&lt;div class=&#34;mermaid&#34;&gt;graph TD
    poller[5-min completion poller] --&gt; map[MAP: extract changed conversations]
    map --&gt;|conversations remain| drain[Drain: mark &#39;draining&#39;, re-fire next cycle]
    map --&gt;|coverage below threshold| retry[Skip REDUCE: re-MAP next cycle]
    map --&gt;|extract set complete| profile{Profile current?}
    profile --&gt;|dirty / schema bump / first run| bootstrap[Bootstrap: hierarchical fold over all history]
    profile --&gt;|current| foldfwd[Fold-forward: fold only aged-out extracts]
    bootstrap --&gt; hash{generation_hash changed?}
    foldfwd --&gt; hash
    hash --&gt;|no| noop[No-op: idempotent skip]
    hash --&gt;|yes| synth[Two-call synthesis: pattern + gated shift]
    synth --&gt; store[(Persisted insights)]
&lt;/div&gt;
&lt;p&gt;&lt;em&gt;MAP — per-conversation structured extraction&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The MAP activity extracts one structured &lt;code&gt;ConversationExtract&lt;/code&gt; per conversation that needs (re)processing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Change detection&lt;/strong&gt;: a per-conversation &lt;code&gt;source_fingerprint&lt;/code&gt; is compared against the persisted extract, so only genuinely changed conversations are re-mapped — every unchanged conversation is skipped before any token is spent&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bounded, isolated parallelism&lt;/strong&gt;: candidate conversations are mapped concurrently under an &lt;code&gt;asyncio.Semaphore&lt;/code&gt;, with Temporal heartbeats as they complete; a failure on one conversation is isolated and never fails the user&amp;rsquo;s run&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No-truncation windowing&lt;/strong&gt;: oversized conversations are split into token-budgeted windows, each extracted separately and then merged — content is never clipped, and the merge deliberately preserves the &lt;em&gt;earliest&lt;/em&gt; framing markers as frozen &amp;ldquo;then&amp;rdquo; anchors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model-facing schema with code-owned provenance&lt;/strong&gt;: extraction is enforced as a Pydantic model via tool-calling structured output; factual provenance the model can&amp;rsquo;t know (conversation UUID, timestamps) is injected by code, not produced by the model&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cold-start draining&lt;/strong&gt;: when more conversations remain than one batch should map, the user&amp;rsquo;s row is set to &lt;code&gt;draining&lt;/code&gt; and the workflow returns &lt;code&gt;in_progress&lt;/code&gt; so the poller re-fires next cycle — bounded batches over poll cycles instead of an unbounded &lt;code&gt;continue_as_new&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fail-closed coverage gate&lt;/strong&gt;: if the per-batch error ratio exceeds a configured threshold, REDUCE is skipped entirely and the failed conversations re-map next cycle — the system never synthesizes insights from partial coverage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;REDUCE — folding extracts into insights&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The REDUCE activity aggregates a user&amp;rsquo;s extracts into the insight blob, backed by a durable &lt;code&gt;PracticeProfile&lt;/code&gt; that keeps the token budget bounded no matter how long the history grows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bootstrap vs. fold-forward&lt;/strong&gt;: on first run, a schema bump, or an invalidated profile, a chronological hierarchical reduce folds &lt;em&gt;all&lt;/em&gt; history into a compacted profile (running outline + principle/dimension tallies + frozen earliest anchors). On every subsequent run it &lt;em&gt;folds forward&lt;/em&gt; — advancing a &lt;code&gt;covered_through&lt;/code&gt; watermark and folding only the extracts that have aged out of the granular recent window&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recent-at-full-granularity, history-compacted&lt;/strong&gt;: the most recent extracts are read verbatim (they drive the current pattern); everything older lives in the bounded profile outline. This is what caps cost as history grows without number&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Idempotency, three ways&lt;/strong&gt;: a SHA-256 &lt;code&gt;generation_hash&lt;/code&gt; over the inputs short-circuits any run whose inputs are unchanged; the fold happens &lt;em&gt;before&lt;/em&gt; the watermark is mutated, so a failure can never advance &lt;code&gt;covered_through&lt;/code&gt; past an unsummarized batch; and the insight write plus its side effects commit in a single caller-owned transaction&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two-call gated synthesis&lt;/strong&gt;: a pattern unit (through-line + unexplored-principle) and a gated &amp;ldquo;what&amp;rsquo;s shifted&amp;rdquo; unit run concurrently with per-call failure isolation; the expensive shift analysis is gated behind minimum-history and minimum-timespan thresholds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sharpen, don&amp;rsquo;t whiplash&lt;/strong&gt;: prior observations are fed back into the synthesis prompt so insights evolve incrementally rather than lurching between runs, with section-level freshness resolving fresh output against the previous result&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;ai-security-and-trust-boundaries&#34;&gt;AI Security and Trust Boundaries&lt;/h3&gt;
&lt;p&gt;Because extracted output is persisted and re-fed into later prompts, I treated the LLM as an untrusted component and hardened the boundary on both sides of every call:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User content is DATA, not instructions&lt;/strong&gt;: transcript text has its angle brackets escaped before entering the prompt, so a message can&amp;rsquo;t close the &lt;code&gt;&amp;lt;transcript&amp;gt;&lt;/code&gt; wrapper or inject markup the model would treat as instructions — the transcript stays human-readable while being inert&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM output is allow-listed before persistence&lt;/strong&gt;: every principle and behavioral dimension the model emits is checked against the controlled catalog/enum and dropped if it isn&amp;rsquo;t a member, so a prompt-injection attempt can&amp;rsquo;t poison durable state with fabricated vocabulary&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provenance is code-owned&lt;/strong&gt;: conversation identity and timestamps are injected by code and never taken from model output, so the model cannot misattribute an extract to another conversation or user&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fail-closed everywhere&lt;/strong&gt;: partial MAP coverage, a failed profile fold, or a failed core synthesis all refuse to persist a &amp;ldquo;success&amp;rdquo; — the run is marked for retry with prior state left intact, rather than shipping a confidently-wrong result&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;technical-implementation&#34;&gt;Technical Implementation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Temporal orchestration&lt;/strong&gt;: MAP and REDUCE are activities with explicit start-to-close (10 min) and heartbeat (5 min) timeouts and bounded retry policies; the workflow is triggered per-user by a completion poller, and REDUCE re-raises retryable failures so Temporal — not a swallowed exception — owns the retry&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Structured output via tool-calling&lt;/strong&gt;: every LLM interaction (extraction, profile fold, pattern synthesis, shift synthesis) is a schema-validated tool call into a Pydantic model, eliminating free-text parsing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Langfuse observability&lt;/strong&gt;: one trace per conversation in MAP (session-scoped to the conversation, each window a separate generation) and one per-user trace for the whole REDUCE, giving prompt/latency/cost visibility at both granularities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pure-function core&lt;/strong&gt;: partitioning, window/epoch merging, tally accumulation, and the generation-hash preimage are all pure functions, unit-tested independently of the database and the LLM — the deterministic logic is provable without touching a model&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Config-driven budgets&lt;/strong&gt;: token budgets, concurrency limits, batch sizes, the coverage error-ratio threshold, and the shift-gating thresholds are all externalized settings rather than hard-coded constants&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;key-design-decisions&#34;&gt;Key Design Decisions&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Incremental fold-forward over full re-synthesis:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Re-reading a user&amp;rsquo;s entire history on every run is simple but scales cost with total history — untenable at a minutes-level cadence&lt;/li&gt;
&lt;li&gt;The durable profile + watermark makes per-run cost proportional to &lt;em&gt;new&lt;/em&gt; conversations, so a long-tenured user costs no more per run than a new one&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Watermark advanced only after a successful fold:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advancing &lt;code&gt;covered_through&lt;/code&gt; before persisting the fold would let a mid-run failure silently drop the unsummarized batch from history forever&lt;/li&gt;
&lt;li&gt;Folding first and mutating second makes the whole REDUCE safely retryable — a post-commit retry sees the advanced watermark and correctly folds nothing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Treat LLM output as untrusted input:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Most pipelines validate the &lt;em&gt;shape&lt;/em&gt; of structured output; here the &lt;em&gt;contents&lt;/em&gt; are allow-listed against a controlled vocabulary before they ever reach durable state&lt;/li&gt;
&lt;li&gt;Combined with transcript escaping and code-owned provenance, this closes the prompt-injection path into persisted, re-prompted data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cold-start drain over &lt;code&gt;continue_as_new&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bounded per-cycle batches that lean on the existing poller are simpler to reason about and observe than a self-continuing workflow, with no loss of eventual completeness&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Fail-closed synthesis:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A coverage gate and retryable-error contract guarantee the system would rather produce &lt;em&gt;nothing&lt;/em&gt; this cycle than a plausible insight synthesized from partial or corrupted inputs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;results--impact&#34;&gt;Results &amp;amp; Impact&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Delivered a longitudinal insight-synthesis layer that distills a user&amp;rsquo;s full conversation history into three evolving, personalized insights (a through-line pattern, an unexplored principle, and how their framing has shifted over time)&lt;/li&gt;
&lt;li&gt;Architected an incremental map-reduce that keeps per-run LLM cost proportional to changed conversations rather than total history, via per-conversation fingerprint change-detection and a durable fold-forward profile&lt;/li&gt;
&lt;li&gt;Made the pipeline idempotent under Temporal&amp;rsquo;s at-least-once execution — generation-hash short-circuiting, fold-before-mutate watermarking, and single-transaction side effects eliminate false success and double-counting&lt;/li&gt;
&lt;li&gt;Hardened the LLM trust boundary end-to-end: prompt-injection escaping of user content, controlled-vocabulary allow-listing of persisted model output, and code-owned provenance&lt;/li&gt;
&lt;li&gt;Guaranteed lossless processing of arbitrarily long conversations through token-budgeted windowing that splits, extracts, and merges without truncation&lt;/li&gt;
&lt;li&gt;Instrumented full Langfuse observability at both conversation and user granularity for prompt, latency, and cost tracing&lt;/li&gt;
&lt;li&gt;Isolated all deterministic logic into a pure-function core with dedicated unit tests, keeping correctness verifiable independently of the LLM&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;technologies&#34;&gt;Technologies&lt;/h3&gt;
&lt;p&gt;Python, Temporal, Structured Output (tool-calling), Pydantic, asyncio, Langfuse,
PostgreSQL, SQLAlchemy, SHA-256 idempotency, Prompt-Injection Hardening&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>Your Extraction POC Works on 10 Documents. Here&#39;s Why It Dies on 10,000.</title>
      <link>https://mwylot.net/post/extraction-poc-10-documents/</link>
      <pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://mwylot.net/post/extraction-poc-10-documents/</guid>
      <description>&lt;p&gt;I build the extraction pipelines that have to hold up on the whole corpus, not the slice in the demo. Which usually means I get called in after the celebration. The vendor demo ran clean: a dozen hand-picked PDFs went in, structured fields came out, the fields were correct, and everyone in the room exhaled. Somebody said the word &amp;ldquo;magic.&amp;rdquo; A six-figure number got attached to a roadmap. Then the pipeline met the real corpus — the ten thousand documents the business actually owns, the ones nobody curated — and the output quietly fell apart. Not with an error. With confident, plausible, wrong results that nobody caught until they were already downstream.&lt;/p&gt;
&lt;p&gt;Here is what that demo actually proved: the model can do the task once, on inputs chosen to make it look good. That is a real fact, and it is almost never the fact in question. The model can extract the field. Modern models are genuinely good at reading a document and returning structure. What the demo proved nothing about is the only question that matters at scale: will this be true, cheaply, every single time, when part of the batch fails and the whole thing gets retried at two in the morning? &amp;ldquo;Can the model do this&amp;rdquo; is a research question, and it&amp;rsquo;s usually settled before you walk in the door. &amp;ldquo;Will this hold, at cost, under failure, across the full ugly corpus&amp;rdquo; is the entire job. Those are different disciplines. Confusing them is what turns a signed-off POC into a write-off.&lt;/p&gt;
&lt;h2 id=&#34;the-gap-is-engineering-not-intelligence&#34;&gt;The gap is engineering, not intelligence&lt;/h2&gt;
&lt;p&gt;When the pipeline cracks, the instinct in the room is always the same: reach for a smarter model. Swap the mid-tier model for the frontier one, add another reasoning step, wait for next quarter&amp;rsquo;s release. I understand the reflex — the failure &lt;em&gt;feels&lt;/em&gt; like the model isn&amp;rsquo;t smart enough. It almost never is.&lt;/p&gt;
&lt;p&gt;The gap between a working demo and a working system is four engineering failures, and not one of them is fixed by a model upgrade. Cost that scales with the wrong thing. Silent truncation. Confident but partial coverage. And no correctness when the system retries itself. A better model runs each of these failure modes more eloquently; it does not remove any of them. That&amp;rsquo;s the part that decides whether the six figures land, and it&amp;rsquo;s the part that&amp;rsquo;s invisible in a demo. These four are predictable. Someone who has actually shipped extraction at scale can look at your corpus and your cadence and tell you which one bites you first — before you write the check, not after.&lt;/p&gt;
&lt;p&gt;Let me take them one at a time. Each has a fix, and the fix is something you build, not something you buy.&lt;/p&gt;
&lt;h2 id=&#34;failure-one-your-cost-scales-with-the-wrong-thing&#34;&gt;Failure one: your cost scales with the wrong thing&lt;/h2&gt;
&lt;p&gt;On ten documents, cost is a rounding error. You will never notice it, which is exactly why the demo hides it. On ten thousand documents reprocessed at real cadence — every hour, every few minutes, forever — cost is the line item that quietly ends the project. Someone in finance eventually asks why the inference bill looks like a second headcount, and there is no good answer, because the pipeline was built to re-read the entire world on every run.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the trap: the naive design&amp;rsquo;s cost scales with your &lt;em&gt;total state&lt;/em&gt;, not with &lt;em&gt;what changed&lt;/em&gt;. It reprocesses everything because it has no idea what&amp;rsquo;s new.&lt;/p&gt;
&lt;p&gt;I built a pipeline for a US enterprise&amp;rsquo;s public-facing Socratic AI companion — a system that reads each user&amp;rsquo;s entire conversation history and distills a few durable, evolving personal insights. It runs continuously, per user, on a five-minute Temporal schedule. The naive version of this is a cost catastrophe: re-read every conversation for every user every five minutes, forever. So it doesn&amp;rsquo;t. Before a single token is spent, each conversation is fingerprinted — a SHA-256 hash over its message count and its latest timestamps — and any conversation that hasn&amp;rsquo;t changed since the last run is skipped outright. The extraction stage only ever touches conversations that actually moved. On top of that, the whole synthesis step is short-circuited by a second hash: if none of the relevant inputs changed, there is no model call at all. And the profile it maintains is deliberately bounded — it does not grow with how long someone has been a user.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not going to quote you a cost ratio, because the honest answer is that the savings depend on your traffic. What I will tell you is that this is a &lt;em&gt;design property&lt;/em&gt;, not a tuning parameter. Cost proportional to change is something you architect in at the start. You cannot bolt it onto a pipeline that was built to re-read the world, and you certainly cannot buy it from a better model.&lt;/p&gt;
&lt;h2 id=&#34;failure-two-silent-truncation-the-bug-you-never-see&#34;&gt;Failure two: silent truncation, the bug you never see&lt;/h2&gt;
&lt;p&gt;This is the failure I most want to catch before anything ships — and the one a demo is structurally unable to show you.&lt;/p&gt;
&lt;p&gt;Every other failure at least announces itself eventually — a bill, a crash, an empty result. Silent truncation doesn&amp;rsquo;t. It produces a confident, well-formed, completely reviewable answer derived from part of the input. A real document — a long conversation, a dense report, a full transcript — doesn&amp;rsquo;t fit the model&amp;rsquo;s context window. The naive pipeline clips it to fit. No error is raised. No warning is logged. The model dutifully extracts structure from the fraction that happened to fit — everything past the cutoff silently gone — and returns it looking exactly like a correct result. It passes human review, because a reviewer checks whether the output is &lt;em&gt;plausible&lt;/em&gt;, not whether the input was &lt;em&gt;complete&lt;/em&gt;. You cannot see what was silently dropped.&lt;/p&gt;
&lt;p&gt;At ten documents, none of them overflow, so this bug is invisible in the demo by construction. At ten thousand, the long ones always exist, and they are usually your most important records.&lt;/p&gt;
&lt;p&gt;The fix is not subtle, but it has to be deliberate. In the companion pipeline, an oversized conversation is split into sixty-thousand-token windows using a deterministic size estimate, each window is extracted independently, and the results are merged losslessly. The rule that matters is the one about failure: &lt;em&gt;all&lt;/em&gt; windows are required. If any single window fails to extract, the entire conversation is marked as errored and nothing partial is ever persisted. There is no code path that writes half a document and calls it done. That&amp;rsquo;s what &amp;ldquo;no truncation&amp;rdquo; actually means as an engineering commitment — not &amp;ldquo;we made the window bigger,&amp;rdquo; but &amp;ldquo;the system structurally cannot ship a fraction of a document as if it were whole.&amp;rdquo;&lt;/p&gt;
&lt;h2 id=&#34;failure-three-confident-and-wrong-about-half-of-it&#34;&gt;Failure three: confident, and wrong about half of it&lt;/h2&gt;
&lt;p&gt;Here is a fact about running anything at scale: some fraction of every batch fails. A model call times out, a document is malformed, a rate limit trips. On ten inputs you can pretend this away. On ten thousand, failure isn&amp;rsquo;t an exception, it&amp;rsquo;s a rate.&lt;/p&gt;
&lt;p&gt;The dangerous move — the one I see constantly — is to treat partial success as success: take whatever came back from the batch, synthesize a result from it, and ship it labeled &amp;ldquo;complete.&amp;rdquo; Now you have a confident summary that silently omits whatever failed, and nobody downstream knows a piece is missing. That&amp;rsquo;s not a smaller version of the right answer. It&amp;rsquo;s a wrong answer wearing the right answer&amp;rsquo;s clothes.&lt;/p&gt;
&lt;p&gt;The pipeline I built fails closed instead, and it does so at two levels. At the coarse level there&amp;rsquo;s an explicit coverage gate: if the failure rate in a batch exceeds twenty percent, the synthesis stage does not run at all — the run returns &amp;ldquo;in progress&amp;rdquo; and the failed conversations get re-processed on the next cycle. But the gate is the blunt guard, not the real guarantee. The guarantee is structural: no conversation is ever recorded as covered until its extract has actually been folded in. A watermark tracks how much history has genuinely been synthesized, and it advances only over what succeeded — so a conversation that failed this cycle is never quietly marked done. It simply comes back next cycle. The system would rather tell you &amp;ldquo;not finished yet&amp;rdquo; than hand you a confident result that&amp;rsquo;s missing a piece it can&amp;rsquo;t name. That is a product decision as much as an engineering one, and it&amp;rsquo;s the kind of decision that only gets made after someone has been burned by the alternative. Correct under failure is a posture you choose on purpose.&lt;/p&gt;
&lt;h2 id=&#34;failure-four-no-correctness-when-things-retry&#34;&gt;Failure four: no correctness when things retry&lt;/h2&gt;
&lt;p&gt;Production extraction doesn&amp;rsquo;t run as a script on your laptop. It runs on distributed execution engines, and those engines retry. This is not a bug in them — it&amp;rsquo;s the contract. Temporal, which is what I use for durable orchestration, executes activities &lt;em&gt;at least once&lt;/em&gt;: a step that already completed can be run a second time after a worker crash or a timeout, because the engine couldn&amp;rsquo;t confirm the first attempt finished. At-least-once is the price of durability. It means every step in your pipeline must assume it may execute twice.&lt;/p&gt;
&lt;p&gt;A pipeline that ignores this double-counts. It folds the same batch of extractions in twice, or it marks a half-finished run as complete because the retry landed on the wrong side of a state change. These bugs are miserable to reproduce because they only appear under exactly the failure timing the demo never experiences.&lt;/p&gt;
&lt;p&gt;So idempotency isn&amp;rsquo;t a nice-to-have here, it&amp;rsquo;s load-bearing, and in the companion pipeline it&amp;rsquo;s enforced three ways. That &amp;ldquo;covered-through&amp;rdquo; watermark is advanced &lt;em&gt;only after&lt;/em&gt; a fold has successfully completed — the fold runs and is allowed to fail &lt;em&gt;before&lt;/em&gt; the watermark ever moves, so a crash can never skip a batch by advancing state past work that didn&amp;rsquo;t happen. A generation hash over the run&amp;rsquo;s actual inputs short-circuits any run whose inputs haven&amp;rsquo;t changed, so a spurious retry does no synthesis at all. And retryable failures are deliberately re-raised so that Temporal owns the retry, rather than being swallowed and papered over. The orchestration engine is configured to match: bounded automatic retries, heartbeats on the long-running steps so a stalled worker is detected instead of hanging, and — deliberately — a poll-driven drain in bounded per-cycle batches rather than a cleverer self-continuing construct, because a system you can reason about and observe beats a system that&amp;rsquo;s elegant and opaque.&lt;/p&gt;
&lt;p&gt;One more property rides on the same discipline. This pipeline persists a profile and feeds it back into the next run&amp;rsquo;s prompt, so a single poisoned extract could otherwise steer every future cycle. Nothing reaches persisted state unvetted: model output is allow-listed before it&amp;rsquo;s stored — emitted principles checked against a controlled catalog, behavioral dimensions normalized against a fixed vocabulary, anything out-of-vocabulary dropped. The state that gets re-prompted is state the system already vouched for. Correct under retry and secure by design turn out to be the same instinct — never trust output you didn&amp;rsquo;t verify, whether the threat is a double execution or a prompt injection — and both are things you can point to in the code, not things you hope for.&lt;/p&gt;
&lt;h2 id=&#34;scale-is-a-discipline-not-a-trick&#34;&gt;Scale is a discipline, not a trick&lt;/h2&gt;
&lt;p&gt;If all of this only ever showed up in one pipeline, you could dismiss it as one clever hack for one weird problem. It isn&amp;rsquo;t. It&amp;rsquo;s the same discipline, and I watch it recur across domains that have nothing to do with each other.&lt;/p&gt;
&lt;p&gt;For a sports-media company generating automated broadcast narratives, the pipeline processes hundreds of games a season across nine narrative categories each. Same problem shape, different clothes. Cost discipline shows up as prompt caching: one large system prompt — rosters, eighty-seven stat types, the full speech-to-text transcript, tens of kilobytes of it — is cached once and reused across all nine parallel category calls for a game, so only the first call pays full freight and the rest hit cache reads. Coverage discipline shows up as blast-radius control: failure is isolated per category, per element, per game, so one empty category never sinks the broadcast. And correctness shows up as an independent judge model that verifies every extracted element before it survives to the final curated set. I deliberately tore out an earlier framework-heavy design and rebuilt it on plain async orchestration for direct control over exactly these concerns.&lt;/p&gt;
&lt;p&gt;For a civil-engineering firm generating site-assessment reports, hundreds of data points per report flow through an async work queue with an explicit state machine — pending, processing, done, failed — tracked in a real database. It is the least glamorous thing I could describe to you, and it is exactly the thing that turns &amp;ldquo;it worked when I ran it&amp;rdquo; into &amp;ldquo;I can tell you precisely which of the N items succeeded, which failed, and why.&amp;rdquo; Every generated section carries provenance back to a source file and page number, and the whole thing runs through human verification checkpoints. The unglamorous state machine is the product.&lt;/p&gt;
&lt;p&gt;Three industries, one shape. That&amp;rsquo;s how you know it&amp;rsquo;s a practice and not a party trick.&lt;/p&gt;
&lt;h2 id=&#34;why-a-poc-has-to-predict-production&#34;&gt;Why a POC has to predict production&lt;/h2&gt;
&lt;p&gt;The reason the demo lies is now precise: it exercises none of the four failure modes. It runs short, curated, low-failure, single-pass inputs, which is to say it runs the exact conditions under which all four failures are invisible. Of course it looked like magic. It was staged in the one environment where the hard problems don&amp;rsquo;t exist.&lt;/p&gt;
&lt;p&gt;A POC worth the name does the opposite on purpose. It deliberately runs the ugliest, longest, highest-failure inputs in the corpus — the documents that overflow the window, the batches with a bad failure rate, the load that makes the retry timing actually happen — and it reports back on the four things that predict production: what this costs at real cadence, what happens to the documents too big to fit, what the system does when a fifth of the batch fails, and whether it stays correct when every step runs twice. That&amp;rsquo;s the thing I build first, and it&amp;rsquo;s the differentiator worth paying for: a proof of concept that tells you the expensive truth cheaply and early, while the number on the roadmap is still an estimate and not a sunk cost. A POC that de-risks the six-figure bet before you place it is a fundamentally different artifact from a demo that makes the bet feel safe.&lt;/p&gt;
&lt;h2 id=&#34;each-of-these-is-its-own-way-to-die&#34;&gt;Each of these is its own way to die&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve compressed four failure modes into one essay, and each of them deserves its own. Trusting an extracted value as if it were a fact — when to gate, verify, and refuse — is a whole discipline. The data backbone beneath production AI, the state machines and provenance that make the intelligence trustworthy, is the depth that everything else stands on. Knowing when a problem wants a deterministic pipeline versus an agentic one — and refusing to reach for autonomy where control is cheaper and safer — is its own decision with its own failure modes. And what an agentic system actually costs to &lt;em&gt;run&lt;/em&gt;, once it&amp;rsquo;s making its own choices, is a question most teams don&amp;rsquo;t ask until the bill arrives. Each gets its own full treatment. Consider this the map.&lt;/p&gt;
&lt;h2 id=&#34;before-more-money-goes-in&#34;&gt;Before more money goes in&lt;/h2&gt;
&lt;p&gt;So here&amp;rsquo;s the honest question. If your extraction POC only ever ran on the easy documents — the curated dozen, the ones that fit, the batch that happened not to fail — and nobody has yet pointed it at the full, ugly corpus at real cadence, then you don&amp;rsquo;t actually know what you have. You have a demonstration that the model can do the task. You do not yet have evidence about cost, truncation, coverage, or correctness under retry. That gap is precisely the thing worth closing before the next tranche of budget goes in.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not selling you a rescue. I&amp;rsquo;m telling you it&amp;rsquo;s cheaper to find out what breaks in a POC designed to break it than to find out in production, in front of your boss, with the six figures already spent. If that&amp;rsquo;s the conversation you&amp;rsquo;d rather have now than later, that&amp;rsquo;s the one I&amp;rsquo;m good at.&lt;/p&gt;</description>
    </item>
    
  </channel>
</rss>
