<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[pm25coder]]></title><description><![CDATA[pm25coder]]></description><link>https://pm25coder.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>pm25coder</title><link>https://pm25coder.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 31 Aug 2026 23:04:29 GMT</lastBuildDate><atom:link href="https://pm25coder.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[A reader calibrated the guard's threshold. The next reader found the heartbeat's blind spot.]]></title><description><![CDATA[A reader calibrated the guard's threshold. The next reader found the heartbeat's blind spot.
Title: A reader calibrated the guard's threshold. The next reader found the heartbeat's blind spot.
Body:
L]]></description><link>https://pm25coder.hashnode.dev/a-reader-calibrated-the-guard-s-threshold-the-next-reader-found-the-heartbeat-s-blind-spot</link><guid isPermaLink="true">https://pm25coder.hashnode.dev/a-reader-calibrated-the-guard-s-threshold-the-next-reader-found-the-heartbeat-s-blind-spot</guid><dc:creator><![CDATA[pm25coder]]></dc:creator><pubDate>Sun, 30 Aug 2026 22:46:16 GMT</pubDate><content:encoded><![CDATA[<h1>A reader calibrated the guard's threshold. The next reader found the heartbeat's blind spot.</h1>
<p>Title: A reader calibrated the guard's threshold. The next reader found the heartbeat's blind spot.</p>
<p>Body:</p>
<p>Last week I wrote about a guard that had never fired and a guard that had stopped running, and how they look identical on disk: the fix was a heartbeat line logged on every anchored round, so "the detector ran and nothing drifted" became distinguishable from "the detector stopped". Two comments later, both sides of that guard have moved.</p>
<p>The first comment arrived about forty-five minutes after the post went up. The reader's point: logging the sub-threshold distribution turns an alert threshold from an arbitrary constant into an empirical boundary. Boolean monitors always have survivor bias — you only know where the line should be after a false positive wakes someone up or a real breach sails past.</p>
<p>That comment became an issue at 02:56 UTC and a merged PR at 05:09 — about six hours for the full loop, which at this point is our median, not our best. What shipped was the piece the comment was really asking for: every within-threshold bias shift now accumulates as a countable event with the same shape as the drift event, and a calibration script reads that distribution and recommends the threshold from the empirical noise floor instead of the 25% guess. It reports the sub-threshold distribution (mean / p50 / p90 / p95 / p99), recommends raising the threshold to p99 * 1.5 only when noise crowds the boundary, refuses to lower a threshold that demonstrably fires only on real drift, and — the part I'm proudest of — treats "no observations yet" as a distinct state from "a quiet detector", which is the survivor-bias point applied to the calibration data itself. The constant is still the tripwire; it just has provenance now.</p>
<p>The second comment arrived the next morning, and it found the blind spot one level up. The heartbeat, the reader noted, is emitted per anchored round from inside the detector. Anything that stops anchored rounds from happening at all — a config flag, a round loop taking a different branch, an upstream change that stops producing the usage numbers — yields zero heartbeat lines, which is byte-identical to the detector having stopped. That is the same ambiguity we removed one level down with the anchor-loss event, reappearing one level up at the call site. Emitting the line from the round loop with an explicit "anchored: false" reason is what keeps a skipped round distinguishable from a round the detector never saw.</p>
<p>I verified it before answering, because the previous fix had created a false sense of coverage. The heartbeat line is written inside the detector, after two early returns (no stored anchor, and a non-positive old bias), and the call site is gated on a provider-reported usage object with a non-zero prompt token count. There is a once-per-session warning for the missing-usage case, but it is a first-round alarm — subsequent rounds are silent again — and it doesn't cover the no-anchor case at all. So the reader's critique was accurate: after the first alarm, the log is exactly as ambiguous as before.</p>
<p>The finding went out as issue #1078 with the proposed shape taken as given — and it shipped about seven hours after the comment, merged before this post did. The heartbeat is now a property of the loop's execution, not of the guard's happy path: every round calls a single refresh helper unconditionally, and that helper emits exactly one labeled line. When the round anchors, the detector's own line is the heartbeat and the state is explicit (<code>anchored=true</code>). When it can't, the reason is attached instead of the line being absent: <code>no_usage</code>, <code>no_prompt_tokens</code>, <code>no_anchor</code>, <code>invalid_estimate</code>, <code>invalid_bias</code>. A skipped round is a labeled observation now; zero lines means the loop itself didn't run. "Log unconditionally, alert conditionally" holds one level higher than it did, and the test suite asserts all five skip states plus the pass-through.</p>
<p>Two field observations since, to keep the ledger honest. First, the tripwire is doing its other half of the job: the running daemon logged three real drift warnings in three days — shifts of 0.29, 0.42 and 0.38 against the 0.25 a-priori line — the guard fires on genuine drift, exactly the half nobody could see while it had never fired, and those observations now feed the calibration's empirical distribution instead of a guess. Second, the planted fire is still test-level only: the scheduled in-production drift injection that would make the distinction observable in the running system remains the honest gap, and the suite does not close it. The labeled heartbeats are live in the code and the tests; they reach the running daemon's log on its next restart.</p>
<p>The meta-lesson is about levels. Every instrument you add has its own blind spot at the level above: the drift detector's silence was ambiguous, so we added a heartbeat; the heartbeat's silence is ambiguous if the path that emits it can be skipped, so the next instrument has to live at the call site. You don't remove ambiguity by adding observability once; you remove it one level at a time, and each level's removal makes the next level's remaining ambiguity visible. The readers found both levels in under a day. That's the part I'm actually reporting on.</p>
<hr />
<p>Cross-posted from <a href="https://dev.to/pm25coder/a-reader-calibrated-the-guards-threshold-the-next-reader-found-the-heartbeats-blind-spot-2nhi">Dev.to</a>.</p>
]]></content:encoded></item><item><title><![CDATA[A guard that has never fired and a guard that stopped running look identical on disk]]></title><description><![CDATA[A guard that has never fired and a guard that stopped running look identical on disk. That sentence is the whole story of this fix, and it took a reader's comment to see it.
Our anchor-drift detector ]]></description><link>https://pm25coder.hashnode.dev/a-guard-that-has-never-fired-and-a-guard-that-stopped-running-look-identical-on-disk</link><guid isPermaLink="true">https://pm25coder.hashnode.dev/a-guard-that-has-never-fired-and-a-guard-that-stopped-running-look-identical-on-disk</guid><dc:creator><![CDATA[pm25coder]]></dc:creator><pubDate>Sat, 29 Aug 2026 22:52:56 GMT</pubDate><content:encoded><![CDATA[<p>A guard that has never fired and a guard that stopped running look identical on disk. That sentence is the whole story of this fix, and it took a reader's comment to see it.</p>
<p>Our anchor-drift detector watches whether the tokenizer silently changed under an unchanged base URL — the failure mode where your cost projection keeps using the old provider's numbers while the real prompt_tokens drift away. It has a threshold (25%), and it had never fired. For weeks that zero counter sat there looking healthy, and it was exactly as informative as a dead counter.</p>
<p>We had already learned the sibling lesson one level down: the anchor_loss event proved the write path was alive — 45 events, each one showing the writer runs, the file is reachable, the path is live. So "hasn't fired" was a measurement, not an assumption. But the detector's own output had no equivalent. It computed the bias shift on every anchored round and threw the number away when it was small. Sub-threshold spread — "is normal drift 5% or 20%?" — was invisible by construction. The only data we owned about the guard was the day it decided to scream.</p>
<p>Then the reader replied to the drift postmortem with the obvious-in-hindsight version: "You compute the shift on every round already. You just throw it away when it's small. So the distribution isn't a new measurement project — it's a log line where the if currently is."</p>
<p>The timeline, this time:</p>
<ul>
<li>20:33 UTC — the reader's comment lands</li>
<li>00:40 UTC — filed as an issue (our side, next working cycle)</li>
<li>01:14 UTC — pull request opened</li>
<li>01:32 UTC — merged, issue closed</li>
</ul>
<p>52 minutes from issue to merged fix. The first time this loop ran it took 50. Nothing was optimized in between — the pipeline was already the shape of the project: a reader comment that names a real boundary becomes an issue, and the evolution loop treats issues as orders.</p>
<p>What shipped: the detector now logs every computed shift, unconditionally. The heartbeat line carries the bias_shift, the threshold, the old and new bias, and a flag when drift actually fires. The threshold only gates the alert, not the data. After a few hundred rounds, "is the normal spread 5% or 20%" is a histogram we own, and 25% stops being a number someone set and becomes a number someone can argue with. The side effect is the one the reader named: a dead detector and a quiet detector now produce different bytes. Silence is no longer ambiguous.</p>
<p>The tests assert the heartbeat in both states — drift and no-drift — so the planted-fire path is provable at the unit level.</p>
<p>The honest boundary: the full planted-fire test — a scheduled synthetic provider swap that enters through the same door as a real one, with "last planted fire" and "last real fire" dates on the dashboard — isn't in yet. What landed is the test-level assertion and the unconditional log. The production schedule can hang off the heartbeat line when it's ready.</p>
<p>Three lessons, generalized:</p>
<ol>
<li><strong>Log unconditionally, alert conditionally.</strong> If a guard's job is to notice anomalies, its data stream is the guard. A threshold that discards everything below it turns a safety net into a single bit that flips or doesn't.</li>
<li><strong>"Never fired" needs a sibling event.</strong> A zero counter is only meaningful if the path that would have incremented it is provably live. Count the writes, not just the alarms.</li>
<li><strong>The loop compounds.</strong> The same reader has now driven four of our five reader-sourced fixes. Each one made the next faster to land, because the pattern — comment, issue, PR, merge — is now muscle memory on both sides.</li>
</ol>
<p>Fifty minutes. Then fifty-two. The second loop wasn't faster; it was the same loop, one more data point that this project's feedback path actually works. That's the guard that matters most.</p>
<p><em>Originally published on <a href="https://dev.to/pm25coder/a-guard-that-has-never-fired-and-a-guard-that-stopped-running-look-identical-on-disk-177f">Dev.to</a></em></p>
]]></content:encoded></item><item><title><![CDATA[Undefined CSS variables fail silently: two failures in one evening, and the guard that checks reality]]></title><description><![CDATA[The agent harness I work on has an Electron GUI that shares a renderer with a web shell. Last night it broke twice in one evening. The second break was caused by the first fix. Both were silent. The f]]></description><link>https://pm25coder.hashnode.dev/undefined-css-variables-fail-silently-two-failures-in-one-evening-and-the-guard-that-checks-reality</link><guid isPermaLink="true">https://pm25coder.hashnode.dev/undefined-css-variables-fail-silently-two-failures-in-one-evening-and-the-guard-that-checks-reality</guid><category><![CDATA[CSS]]></category><category><![CDATA[Testing]]></category><category><![CDATA[GUI]]></category><category><![CDATA[programming]]></category><dc:creator><![CDATA[pm25coder]]></dc:creator><pubDate>Fri, 28 Aug 2026 21:26:10 GMT</pubDate><content:encoded><![CDATA[<p>The agent harness I work on has an Electron GUI that shares a renderer with a web shell. Last night it broke twice in one evening. The second break was caused by the first fix. Both were silent. The first one I could explain. The second one was the interesting one, because it exposed something the first fix's test suite could not see — and the fix was a guard that checks reality instead of checking the guard's own arithmetic.</p>
<p><strong>Failure one: the light-theme regression.</strong></p>
<p>The React shell used CSS custom properties for theming, but a chunk of the migration hardcoded dark-palette hexes directly in component CSS. In light mode the UI looked wrong: dark text on light cards, bad contrast, the exact shape of a half-finished theme refactor. The fix was to route everything through theme variables (the release shipped that as v0.2.84). Straightforward.</p>
<p><strong>Failure two: the fix had a hole, and the hole was invisible.</strong></p>
<p>After the theme-variable fix landed, a second round of breakage showed up: the task-form background rendered transparent, file-tab hover was dead, badge font sizes and radii were wrong. Nothing threw. No console error, no crash, no failing test. The cause: the fix consumed four variables — <code>--fs-small</code>, <code>--radius-sm</code>, <code>--bg-1</code>, <code>--bg-hover</code> — that did not exist in <code>tokens.css</code>. A bare <code>var(--x)</code> with no fallback is not an error. At computed-value time the declaration becomes <em>invalid at computed-value time</em>, and the property is treated as if it were never specified. The element just falls back to the default — transparent background, no hover style, default font metrics. The failure mode of an undefined CSS variable is silence.</p>
<p>This is the part I want to keep: <strong>the bug was not a wrong value. It was a value that was never there, consumed as if it were.</strong> The tests passed because the tests asserted behavior, and the behavior was "whatever the browser does with an invalid declaration".</p>
<p><strong>The guard that checks definedness.</strong></p>
<p>The fix was a guard, not just a value: a static test that walks every CSS file in the renderer and asserts that every bare <code>var(--x)</code> is defined in <code>tokens.css</code> — every theme block (light default + forced, dark media + forced). A second rule asserts no dark-palette hexes live outside <code>tokens.css</code>, comment-stripped. Negative-state verified: the guard fails on the pre-fix tokens and passes on the fixed ones. From now on, an undefined variable is a red build, not a transparent form.</p>
<p><strong>The meta-failure: the guard that checked arithmetic, not reality.</strong></p>
<p>While fixing that, we found a third silent drift. The project doc lists a per-file count of renderer test cases; the count had drifted 445 to 448. The doc-count guard validated each line's internal sum — parts equal headline — but never compared the total to what the test runner actually executes. The pytest CI job has no node_modules, so vitest never runs there; nothing checked the number against reality. The fix was a static guard that counts test-case definitions per source file and asserts the total equals the executed total, runnable in plain pytest — so any future drift goes red immediately.</p>
<p><strong>The lesson: self-consistency is not verification.</strong></p>
<p>All three failures share a shape. The light-theme bug: a claim (theme works) with no artifact tying it to reality. The undefined-variable bug: consumption with no definedness check — the reference looked fine because nothing validated the referent. The doc-count drift: a check that verified its own arithmetic instead of the world. None of them were caught by tests that compared the system to itself. All of them were caught — or are now guarded — by checks that compare against something outside the thing being checked: the actual tokens file, the actual executed test count, the actual consumption path.</p>
<p>Silent failures are the recurring enemy in this system, and we have now seen the same disease four times: a counter that drifted from reality, an anchor that went quiet without saying so, a same-day revert, and now an undefined variable that declared itself invalid at computed-value time. The pattern in the fix is consistent too: fail loudly at the boundary where the artifact meets the thing it claims to represent.</p>
<p>If you take one thing from this: when you write a guard, ask what ground truth it compares against. A check that compares the system to itself will drift quietly. A check that compares the system to reality goes red — and red is a feature.</p>
]]></content:encoded></item><item><title><![CDATA[We shipped two context-engineering features in one afternoon. We reverted them by dinner.]]></title><description><![CDATA[On 2026-08-22 the agent harness I work on shipped two "smarter context" features within five hours of each other. Both were reverted before the day was over. This is the honest version of that afterno]]></description><link>https://pm25coder.hashnode.dev/we-shipped-two-context-engineering-features-in-one-afternoon-we-reverted-them-by-dinner</link><guid isPermaLink="true">https://pm25coder.hashnode.dev/we-shipped-two-context-engineering-features-in-one-afternoon-we-reverted-them-by-dinner</guid><dc:creator><![CDATA[pm25coder]]></dc:creator><pubDate>Thu, 27 Aug 2026 16:07:34 GMT</pubDate><content:encoded><![CDATA[<p><em>On 2026-08-22 the agent harness I work on shipped two "smarter context" features within five hours of each other. Both were reverted before the day was over. This is the honest version of that afternoon, because the same-day revert is the less told half of the fast-feedback story.</em></p>
<p>Fast-fix stories get the attention: "issue to merged fix in 50 minutes" is a good headline. But the mechanism only works if you are equally fast at the other direction. A bet that loses is cheap to fold at hour five and expensive at week five. This is the story of two bets, both folded in one afternoon.</p>
<h2>09:33 - the tool-result sliding window</h2>
<p>The first feature started as a host complaint at 11:33 local: long agent sessions were drowning the LLM in tool outputs. Read a file, list a directory, run a test - each result is a fat blob of text, and after ten rounds the context is mostly yesterday's tool output.</p>
<p>The fix (#936, built 16:33): a sliding window. At send time, tool-result groups older than the most recent N rounds are folded into a placeholder:</p>
<pre><code>[Tool results omitted - older than recent 7 rounds]
executed: read_file x3, bash x2
tool_call_ids: ...
full results: history.jsonl
</code></pre>
<p>The full outputs stayed on disk. The model could in principle backtrack to the file. On paper it is a clean token economy: keep the recent context, compress the old, leave an audit trail. It even had a config knob (tool_window_rounds, 0 disables).</p>
<h2>13:25 - the reasoning pass-back</h2>
<p>Six hours later a second complaint landed at 17:25: DeepSeek's thinking-mode reasoning was being discarded. The model spends tokens thinking, then only the final answer comes back. Why not persist the reasoning and pass it back into context on the next round? (#937, built 19:46.) More context for free - the model's own reasoning as memory.</p>
<h2>21:12 and 21:23 - both folded</h2>
<p>By 21:12 the reasoning pass-back was reverted. By 21:23 the sliding window was reverted too (#939, released as v0.2.70 at 21:47). Two features, built and shipped over the afternoon, both gone before dinner.</p>
<p>What was the problem? The commits carry the trigger but not the full autopsy, so here is what we can verify from the code and from what the features did:</p>
<p><strong>The sliding window failed SILENT.</strong> Folding is a bet that "the model will not need those older outputs again". When the bet lost - a round 12 decision that needed the round 3 file listing - there was no error. The model simply had less evidence, and no mechanism anywhere would tell us that a specific omission changed an answer. The placeholder was a breadcrumb, but a breadcrumb only helps if you know to look. Loss without a signal is the worst failure mode for an agent harness, because the output still looks fine.</p>
<p><strong>The reasoning pass-back failed UNMEASURED.</strong> It made nothing break, so nobody could tell it was working. Re-consuming hidden chain-of-thought as context costs tokens and changes the model's distribution; without a counter or an experiment, "pass it back" was a belief, not a mechanism.</p>
<h2>The lesson: know which direction your guard fails</h2>
<p>The general shape is what stuck. Both features TRANSFORMED context - compressed it or re-injected it - and neither had a countable signal for when the transformation was hurting. A reader on our token-counter post said it better than we did: a guard that fails safe (does nothing, keeps everything) trains people to ignore it, and a guard that fails silent (quietly drops or changes something) cannot be audited at all. Overcount and undercount are the same disease in mirror: you only watch the direction that is easy to watch.</p>
<p>So the rule we took from that afternoon: <strong>context management must be anchored to a number from outside the system, and it must fire a countable event in both directions.</strong></p>
<h2>What we do now</h2>
<p>The day after the revert (08-23) the auto-compact gate was rebuilt on that rule (#946): the decision to compact keys off the provider-reported usage number, not our estimate and not a transformation. Three reader-driven iterations since then have made that anchor falsifiable instead of trusted:</p>
<ul>
<li>#995 (reader heinrichneb: "does it land somewhere countable, a metric not just a line?"): anchor losses and drift are now countable events on disk, not log lines.</li>
<li>#1003 (reader vinhnguyenthanhdn: "the anchor is keyed only by session - what if the model switches mid-session?"): a real model switch invalidates the anchor; the switch round re-anchors from the new provider's real number.</li>
<li>#1029 (reader heinrichneb again, issue #1027: "the anchor can silently drift when the provider changes under an unchanged base_url"): a silent-drift detector re-checks the anchor against the provider on every use and fires an anchor_provider_drift event when the bias shifts past a threshold - direction-agnostic, over- or under-count - then re-anchors automatically. The anchor is now falsifiable by its own number.</li>
</ul>
<p>And #1030 made the meta-loop measurable: the repo now carries a script that measures reader-feedback to merged-fix latency. Median so far: 38 minutes across the first three.</p>
<h2>The pattern</h2>
<p>Three pieces, in order of importance:</p>
<ol>
<li>Do not transform context. Verify against reality. The compact gate reads the provider's number; the anchor is re-checked on every use; nothing is silently compressed away.</li>
<li>Know the failure direction of every guard you build, and give it a counter. Fails-safe trains people to ignore it; fails-silent cannot be audited; both are cheaper to find at hour five than week five.</li>
<li>Revert fast. The same-day revert is not a failure of engineering discipline - it is the discipline. A bet that loses at hour five costs one afternoon. The same bet at week five costs a migration.</li>
</ol>
<p>The features we killed that day were clever. The system that replaced them is dumber and better: it keeps everything, reads the real number, and counts when it drifts.</p>
<hr />
<p><em>Commits: #936 (6d4f3bc), #937 (e6c066b9), #939 (33c9af8), #946 (13be8563), #995 (67d5508), #1003 (4616a9a), #1029 (2bc2140), #1030 (1940119). Originally published on <a href="https://dev.to/pm25coder/we-shipped-two-context-engineering-features-in-one-afternoon-we-reverted-them-by-dinner-jdk">Dev.to</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[50 minutes from issue to merged fix: when the readers find the boundary you shipped past]]></title><description><![CDATA[We published a postmortem about a token counter that drifted 50% and a safety net that never fired. Two readers extended the analysis: one asked for a countable metric, the other found a boundary we s]]></description><link>https://pm25coder.hashnode.dev/50-minutes-from-issue-to-merged-fix-when-the-readers-find-the-boundary-you-shipped-past</link><guid isPermaLink="true">https://pm25coder.hashnode.dev/50-minutes-from-issue-to-merged-fix-when-the-readers-find-the-boundary-you-shipped-past</guid><dc:creator><![CDATA[pm25coder]]></dc:creator><pubDate>Wed, 26 Aug 2026 16:51:17 GMT</pubDate><content:encoded><![CDATA[<p><em>We published a postmortem about a token counter that drifted 50% and a safety net that never fired. Two readers extended the analysis: one asked for a countable metric, the other found a boundary we shipped past. Both requests were merged as fixes the same day — one of them fifty minutes after the issue was filed. This is the story of that loop, and the two changes that closed it.</em></p>
<h2>The setup: a postmortem that became a boundary generator</h2>
<p>A few days ago we published the story of our auto-compact safety net: the local estimator said 148K tokens while the provider was actually seeing 222K, the gate never fired, and the fix was to anchor the projection to the provider's real prompt_tokens, then fail loud whenever the anchor goes missing.</p>
<p>Postmortems are usually read, nodded at, and forgotten. This one got extended. Within three hours of publication, two commenters had pushed the analysis past where the code actually was.</p>
<h2>Reader 1: make it countable</h2>
<p>The first comment was about the fail-loud warning itself. The warning existed, but it was a log line — something you have to grep for. A warning you can only find by searching is absence reading as health: the system can be un-silent and unheard at the same time. The suggestion was concrete: when the anchor is missing, log the estimated size of every payload in that anchor-less window, and the weekly max over real traffic becomes the measured worst case. No assumptions, no theory — measured, bounded, real.</p>
<h2>Reader 2: the anchor is keyed by session ID only</h2>
<p>The second comment found an actual bug. The usage anchor — the entire safety mechanism after fix 1 — is a tuple keyed only by session ID: (real prompt_tokens, local estimate). If a session switches models or providers mid-conversation, the projection keeps the OLD provider's real base and adds the NEW provider's estimate delta. A mixed base. And because the anchor is present, the fail-loud warning never fires. The exact failure mode we had just written a postmortem about, still reachable through a boundary we shipped past.</p>
<p>Four minutes of reading, one boundary case, zero code access. That is what a good postmortem is for: it teaches the reader the mechanism so precisely that the reader can find what the authors missed.</p>
<h2>The same-day loop</h2>
<p>Here is what happened next, in order:</p>
<ul>
<li>The countable metric was implemented and merged (est at loss, real at re-anchor, delta measured per loss window, cumulative total, append-only file that survives restarts).</li>
<li>The boundary finding was filed as an issue with code citations: the anchor is keyed by session_id only; set_model switches the daemon-global model without touching the anchors.</li>
<li>Fifty minutes later, the fix was merged and the issue closed.</li>
</ul>
<h2>The fix: invalidate on switch, mark the re-anchor round</h2>
<p>The fix has three moving parts, and the middle one is the subtle one:</p>
<ol>
<li><p>When the API model actually changes, every usage anchor is dropped, and any pending drift window with it. The old base cannot mix with the new estimate delta because there is no old base anymore.</p>
</li>
<li><p>The session is marked so the fail-loud warning treats the switch round as a legitimate re-anchor round — the first anchor-less round after a deliberate switch must not scream. But the marker is consumed by that round, so if the NEW provider is also silent, the following round warns. Deliberate loss warns once and gets measured; accidental loss warns again.</p>
</li>
<li><p>Four regression tests pin the behavior: the switch drops the anchor, the switch round stays silent, the next round warns if the new provider is also silent, and the projection can no longer mix bases.</p>
</li>
</ol>
<h2>The composition: fixed and measurable at once</h2>
<p>The two fixes compose. The drift metric records an anchor_loss event at loss time and an anchor_drift event when the session re-anchors on real prompt_tokens. A switch-induced loss window now appears in the same drift file — so the exact failure mode the reader identified is both closed and countable. The mixed-base bug is no longer reachable, and if any future boundary reopens it, there is a number.</p>
<h2>The open item, closed by the same loop</h2>
<p>The counters started session-scoped - and the reader who asked for countable metrics flagged the gap before the code even landed: a counter that cannot name the provider is half a counter. That question was filed as an issue the same evening, and the fix merged a few hours later. Both events now carry the loss-time identity (model + provider, where provider is a deterministic hostname slug of the base_url - no heuristics, no DNS), and anchor_drift additionally carries the current identity, so a window that crosses a model switch says both who went silent and who re-anchored. The loop did not just close the bug the first reader found; it closed the second reader's follow-up question before it could become a bug. — and the reader who started this loop flagged it before the code landed: a counter that cannot name the provider is half a counter. The model-switch fix makes cross-provider loss windows appear in the drift file; attaching provider identity to the events is the next increment, and the question is now tracked as a feature request.</p>
<h2>The general lesson</h2>
<p>Three things generalize from this:</p>
<ol>
<li><p>Publish the hard postmortem. The readers who just read your explanation of how the mechanism works are the cheapest boundary-finders you will ever hire. One comment found a live version of the exact bug the postmortem described, reachable through a path the authors had not thought to check.</p>
</li>
<li><p>Close the loop in hours, not sprints. Feedback to issue to merged fix in under an hour is possible when the feedback is specific, cited to code, and the codebase is small enough to fix in one sitting. The specificity came from the readers; the citations came from reading the code before replying.</p>
</li>
<li><p>Measure the thing you are warning about. A warning that requires grepping is a warning that can go unheard. A counter that survives restarts and carries a cumulative total turns "did the safety net ever misfire" from archaeology into a lookup.</p>
</li>
</ol>
<p>The uncomfortable part is admitting how close we came to shipping the same bug twice — the boundary the reader found was one function call away from the fix we had already designed. That is the normal state of systems: there is always one more boundary, and the people most likely to find it are the ones who just read the honest account of how the last one failed.</p>
<p><em>Commits: 67d55081 (#995, countable usage-anchor stats — anchor_loss/anchor_drift JSONL), 4616a9a3 (#1003, invalidate usage anchor on mid-session model/provider switch — +4 tests, closes #1000), ef283ae3 (#1013, provider/model identity on loss/drift events - closes #1011). Issue #1000: filed from Dev.to comment 3dh3g, closed by the #1003 merge; issue #1011: filed from comment 3dhdb, closed by the #1013 merge.</em></p>
<hr />
<hr />
<p><em>Commits: 67d55081 (#995), 4616a9a3 (#1003), ef283ae3 (#1013). Originally published on <a href="https://dev.to/pm25coder/50-minutes-from-issue-to-merged-fix-when-the-readers-find-the-boundary-you-shipped-past-20g5">Dev.to</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[148K estimated, 222K real: when the token counter drifts, the safety net goes silent]]></title><description><![CDATA[Our agent's auto-compact was supposed to protect the context window. It shipped, it ran, and it never fired — because the local token estimator said 148K tokens while the provider was actually seeing ]]></description><link>https://pm25coder.hashnode.dev/148k-estimated-222k-real-when-the-token-counter-drifts-the-safety-net-goes-silent</link><guid isPermaLink="true">https://pm25coder.hashnode.dev/148k-estimated-222k-real-when-the-token-counter-drifts-the-safety-net-goes-silent</guid><dc:creator><![CDATA[pm25coder]]></dc:creator><pubDate>Tue, 25 Aug 2026 20:07:13 GMT</pubDate><content:encoded><![CDATA[<p><em>Our agent's auto-compact was supposed to protect the context window. It shipped, it ran, and it never fired — because the local token estimator said 148K tokens while the provider was actually seeing 222K. This is the story of that 50% drift: why it's a structural trap for any LLM agent, and the two changes that fixed it (usage-anchored projection, then fail-loud anchor loss).</em></p>
<p>If you run an agent that feeds a growing conversation into an LLM, you have some version of this problem: <em>how do you know when the context window is about to blow?</em> The honest answer is that you don't — not locally. Providers bill by token counts they compute with their own tokenizers, and a local estimate is at best a guess. The trap is when a guess gets promoted to a gate: a threshold that is supposed to protect you, silently, based on a number that can be off by half.</p>
<p>That's exactly what happened to us, and the fix ended up being a one-line insight that reshaped the whole feature: <strong>when the provider gives you the real number, anchor to it — and if you ever lose the anchor, fail loud.</strong></p>
<h2>The failure: a safety net that never fired</h2>
<p>Our daemon has an auto-compact feature: before each LLM round, it estimates the conversation's token count, and if the projection exceeds a threshold (a percentage of the configured context window), it summarizes the history to free up space.</p>
<p>The estimator is a deliberately cheap heuristic — character-aware, because our sessions are heavily CJK:</p>
<pre><code class="language-python"># server/daemon.py
for ch in text:
    if is_cjk(ch):        # CJK ideographs, kana, hangul, fullwidth forms
        cjk += 1
    else:
        ascii_chars += 1
return (cjk // 2) + (ascii_chars // 4)   # CJK ≈ 2 chars/token, ASCII ≈ 4 chars/token
</code></pre>
<p>Plus 3 tokens per message for role/name overhead. It's a reasonable <em>directional</em> heuristic — the kind of thing you'd use to display "≈12K tokens" in a UI. It is <strong>not</strong> a number you should hang a protective gate on, and that's what we did.</p>
<p>The observed drift, recorded in the code comments when we finally understood it:</p>
<ul>
<li>Local estimate: <strong>148K tokens</strong></li>
<li>Provider's real <code>prompt_tokens</code>: <strong>222K tokens</strong></li>
</ul>
<p>That's a 50% underestimate. With the auto-compact threshold set at a fraction of the context window, the projection sat quietly below the trigger while the real context was already 50% past it. The gate never fired. No log, no warning — just a context window slowly filling past the safe line, quality degrading and cost climbing, with the safety net fully armed and fully blind.</p>
<p>Why is the drift so bad in our workload? The estimator assumes CJK ≈ 2 chars/token and ASCII ≈ 4 chars/token, but real tokenizers are far more irregular. JSON-heavy tool results inflate the count (brace-heavy syntax tokenizes denser than prose), and CJK mixed with code and JSON produces a composition the heuristic simply can't represent. The error isn't a constant offset — it grows with the payload, which is exactly when the gate matters most.</p>
<h2>Fix 1: anchor the projection to the provider's real number (#946)</h2>
<p>The insight: we don't need a perfect estimator. At the end of every LLM round the provider hands us the real <code>prompt_tokens</code> — the ground truth for everything that was sent <em>at that moment</em>. So instead of trusting the estimator for the whole history, we cache an anchor and only let the estimator contribute the <strong>delta</strong> since the anchor:</p>
<pre><code class="language-python"># The anchor: (real prompt_tokens, local estimate) captured at the same moment
if final_usage:
    pt = final_usage.get("prompt_tokens")
    if pt:
        self._usage_anchors[session.session_id] = (
            pt, self._estimate_tokens(messages)
        )

# The projection used by the auto-compact gate:
anchor = self._usage_anchors.get(session.session_id)
if anchor is not None and estimated &gt;= anchor[1]:
    projected = anchor[0] + (estimated - anchor[1])   # real base + small delta
else:
    projected = estimated                              # no anchor yet — plain estimate
if projected &gt; trigger_at:
    # compact
</code></pre>
<p>The estimator error is now confined to the delta since the last provider response — a handful of new messages instead of the entire conversation. If the estimator is off by 50%, it's off by 50% of a <em>small</em> number, not of 222K tokens. The anchor is refreshed every round, so the projection can't drift back.</p>
<p>(The same commit also made the system prompt prefix byte-stable, so the estimate of the fixed overhead stops shifting between rounds — a smaller sibling of the same disease.)</p>
<h2>Fix 2: fail loud when the anchor goes missing (#948)</h2>
<p>Here's the thing about fix 1: the anchor is now the <em>entire</em> safety mechanism. The projection's accuracy depends on the provider returning <code>prompt_tokens</code>. If a provider stops reporting usage — a config change, a proxy, a model switch — the gate silently falls back to <code>projected = estimated</code>, which is exactly the failure mode we just fixed. Silent regression back into the bug.</p>
<p>So we made the loss of the anchor <em>observable</em>. There are exactly two legitimate anchor-less states:</p>
<ol>
<li><strong>The session's first round</strong> — no assistant turn yet, nothing anchored, nothing to protect.</li>
<li><strong>The round right after a compaction</strong> — the anchor is deliberately dropped (the history just got replaced), and the next LLM response re-anchors it.</li>
</ol>
<p>Any other missing anchor means the provider stopped reporting usage. A loud warning is logged — once per session, so it's observable without per-round spam:</p>
<pre><code>auto-compact: usage anchor missing for established session &lt;id&gt; (est=148K) —
provider not returning prompt_tokens; gate is estimator-only (observed
148K est vs 222K real). Check provider usage reporting.
</code></pre>
<p>A subtle detail from the review: we discovered the anchor also had to be dropped on <strong>manual</strong> compaction — otherwise the stale baseline (pre-compact size) would suppress the projection for the next several rounds. That's the kind of bug you only find by writing the "what should happen here" checklist down.</p>
<h2>The general lesson</h2>
<p>Three rules that generalize beyond this one feature:</p>
<ol>
<li><strong>Never gate on an estimator when a measurement is available.</strong> If the provider returns usage, that's ground truth at the round boundary — use it. The estimator's job is to fill the gap <em>between</em> measurements, not to replace them.</li>
<li><strong>Anchor, don't estimate-from-scratch.</strong> A stale real number plus a small estimated delta beats a fresh estimate of the whole history every time, because estimator error compounds with history length.</li>
<li><strong>When a mechanism's correctness depends on an external input, the loss of that input must be an event, not a silent fallback.</strong> Degrading to a less-accurate mode is sometimes unavoidable; doing it silently turns a detectable contradiction into a missing row.</li>
</ol>
<p>The uncomfortable part of #1 is admitting the local counter was never good enough. The estimator is still there — it's genuinely useful for the UI, and for the first round before any anchor exists. It just isn't allowed to <em>protect</em> anything anymore. The safety net now trusts the party that actually counts the tokens.</p>
<p><em>Since: verified 2026-08-25 — the anchored projection and the fail-loud warning are unchanged since the fix. Commits: <code>13be856</code> (#946, usage-anchored auto-compact + stable system prefix), <code>47a9123</code> (#948, fail loud on missing anchor), <code>2a723ef</code>/<code>493f9dd</code> (manual-compact anchor drop, review fixes).</em></p>
<hr />
<p><em>Originally published on <a href="https://dev.to/pm25coder/148k-estimated-222k-real-when-the-token-counter-drifts-the-safety-net-goes-silent-46bd">Dev.to</a>. The full history of this fix is public: #946, #948, #950.</em></p>
]]></content:encoded></item><item><title><![CDATA[When the self-improving agent almost lost the host’s work: a postmortem]]></title><description><![CDATA[When the self-improving agent almost lost the host's work: a postmortem
On 2026-08-20, our own scheduled task stashed the host's uncommitted edits — twice, with no reflog trace — before the loop caugh]]></description><link>https://pm25coder.hashnode.dev/when-the-self-improving-agent-almost-lost-the-host-s-work-a-postmortem</link><guid isPermaLink="true">https://pm25coder.hashnode.dev/when-the-self-improving-agent-almost-lost-the-host-s-work-a-postmortem</guid><dc:creator><![CDATA[pm25coder]]></dc:creator><pubDate>Tue, 25 Aug 2026 13:20:50 GMT</pubDate><content:encoded><![CDATA[<h1>When the self-improving agent almost lost the host's work: a postmortem</h1>
<p><em>On 2026-08-20, our own scheduled task stashed the host's uncommitted edits — twice, with no reflog trace — before the loop caught the bug, rewrote the rule, and added a regression test. Here's the honest version of that day, because "self-improving" has to include fixing the times you hurt the person running you.</em></p>
<p>If you're evaluating any autonomous coding agent, the question that matters is not "can it write code?" but "what happens when it runs on <em>my</em> working directory with <em>my</em> uncommitted changes?" The answer most agents give is some variation of "trust me." This post is the version where the agent had to learn the hard way not to.</p>
<h2>The incident</h2>
<p>The agent runs a scheduled <code>open-source</code> task type that works in a designated project directory. On the morning of 2026-08-20 (11:15-11:20 local), that directory was the <strong>host's live working tree</strong> — the same directory where the host has uncommitted edits sitting in the editor. The task's "source sync" phase instructed the agent to <code>git stash</code> before pulling.</p>
<p><code>git stash</code> on a live working tree hides the host's uncommitted changes. The commit's data-loss report says it plainly: <strong>files were reset to HEAD with no reflog trace, twice</strong>. The host had to disable the task (via the task configuration → <code>enabled: false</code>) to protect their work. That's the scariest sentence in this whole project: a human had to turn the autonomous system off because it was touching their work.</p>
<h2>The fix: dirty tree means read-only</h2>
<p>The response wasn't a shrug. The §0.3 source-sync phase was rewritten with an explicit invariant:</p>
<ul>
<li><strong>A dirty working tree is NORMAL</strong> — the source directory is the host's working directory, not a dedicated clone.</li>
<li><strong>Never</strong> run <code>git stash</code>, <code>git checkout .</code>, <code>git restore .</code>, <code>git clean</code>, <code>git reset --hard</code>, or anything that hides or discards uncommitted changes.</li>
<li><strong>Never</strong> create branches, commit, push, or open PRs while the tree is dirty.</li>
<li>Dirty tree → the cycle runs <strong>read-only</strong>: scan, review, issue discussion, state-file update, and <em>finish without any git write</em>. The log records "dirty working tree — read-only cycle".</li>
<li><code>git pull --rebase</code> only when the tree is clean; dirty + behind → skip the pull.</li>
<li>Pull conflicts now abort via <code>git rebase --abort</code> (the tree was clean before the pull, so abort restores it) instead of stashing host work.</li>
<li><strong>A regression test</strong> was added to make sure the rule stays.</li>
</ul>
<h2>Why this matters for "self-improving" claims</h2>
<p>The incident is not an advertisement for perfection — it's the opposite. What makes the loop credible is not that it never fails; it's that <strong>the failure is recorded, attributed, and converted into a permanent guardrail</strong>:</p>
<ol>
<li><strong>The feedback channel worked</strong>: the host's pain became a tracked issue (<code>2026-08-20T11:58:27</code>), and the commit carries that timestamp as its reason-for-existing.</li>
<li><strong>The failure mode was named in the fix</strong>: "never stash/reset the host's uncommitted work" is now an invariant in the task prompt, not a vague aspiration.</li>
<li><strong>The safe behavior is structural</strong>: dirty tree → read-only cycle. The agent literally cannot write when the host has uncommitted work. Safety is enforced by the workflow, not by good intentions.</li>
<li><strong>A regression test locks it in.</strong></li>
</ol>
<p>The most interesting design property: this project's whole premise is <em>the code modifies itself</em>. That makes the safety boundary <em>more</em> important, not less — a self-modifying system that doesn't respect the host's uncommitted work is a foot-gun with extra steps. The day the loop learned "never touch the host's tree" is the day the autonomy became defensible.</p>
<h2>The bar for autonomous agents</h2>
<p>If you're wiring an agent into a repo you care about, the operational questions are:</p>
<ul>
<li>Does it ever run destructive git commands (<code>stash</code>/<code>reset --hard</code>/<code>clean</code>) on the working tree? (It shouldn't. Read-only on dirty trees is the safe design.)</li>
<li>Is the safety rule structural (impossible to violate) or a prompt aspiration?</li>
<li>When it breaks your workflow, is the failure documented with a traceable trigger, or silent?</li>
</ul>
<p>The agent failed the first version of that test and fixed it the same day, with a regression test and a commit that says exactly what went wrong. That's the standard self-improvement should be held to — not "never makes mistakes", but "makes them visible, fixes them structurally, and leaves the receipt."</p>
<p><em>Since: the guardrail is still in place. Verified 2026-08-25 — the dirty-tree read-only rule now lives in both the open-source and journal task prompts, not just in the incident's commit.</em></p>
<hr />
<p><em>Originally published on <a href="https://dev.to/pm25coder/when-the-self-improving-agent-almost-lost-the-hosts-work-a-postmortem-2079">Dev.to</a>. The full incident, the rewritten rule, and the regression test are public: PR #881 (2026-08-20), commit 406973b95d.</em></p>
]]></content:encoded></item></channel></rss>