<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed"/>
    <language>en</language>
    <item>
      <title>Our AI reviewer invented a request. Our producer retried 245 times.</title>
      <dc:creator>GX Cafe LLC</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:31:03 +0000</pubDate>
      <link>https://dev.to/gxcafellc/our-ai-reviewer-invented-a-request-our-producer-retried-245-times-465f</link>
      <guid>https://dev.to/gxcafellc/our-ai-reviewer-invented-a-request-our-producer-retried-245-times-465f</guid>
      <description>&lt;p&gt;We run ~100 LLM agents unattended on local models. Last week we found one&lt;br&gt;
document that had been rewritten &lt;strong&gt;245 times in 5 days&lt;/strong&gt; — every attempt&lt;br&gt;
rejected. A sibling document: 225 times. Combined, about 470 wasted&lt;br&gt;
generations, all burned on the same two files.&lt;/p&gt;

&lt;p&gt;Here is the autopsy, with the actual numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop
&lt;/h2&gt;

&lt;p&gt;Our pipeline is simple: a producer agent writes a document, a reviewer agent&lt;br&gt;
checks it against a contract (minimum length, required sections, no&lt;br&gt;
placeholder junk), and rejected work goes back with fix instructions.&lt;/p&gt;

&lt;p&gt;The rejected document was a key-management (KMS) implementation spec —&lt;br&gt;
4,452 characters, perfectly on-topic. The reviewer's verdict:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The request was a 3-line email triage response (LOCK / VERDICT / REASON),&lt;br&gt;
but the answer is a long KMS spec. Rewrite as &lt;strong&gt;3 lines only&lt;/strong&gt;."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One problem. We grepped the document: the words "LOCK", "VERDICT", and the&lt;br&gt;
name of the triage service &lt;strong&gt;appear zero times&lt;/strong&gt; in it. The reviewer had&lt;br&gt;
invented the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the loop never ended
&lt;/h2&gt;

&lt;p&gt;Two contracts collided:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The reviewer's fix instruction: &lt;em&gt;output 3 lines only&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;The producer's output contract: &lt;em&gt;minimum 600 characters&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No output can satisfy both. So the producer failed the contract, got&lt;br&gt;
re-queued, produced again, failed again — 245 times. Our retry cap counted&lt;br&gt;
&lt;strong&gt;reviews&lt;/strong&gt;, but a contract-failed output never reaches review. The give-up&lt;br&gt;
mechanism existed; it just watched the wrong counter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause: the reviewer never saw the request
&lt;/h2&gt;

&lt;p&gt;Our review prompt contained the artifact body (first 4,000 chars) and the&lt;br&gt;
output format. &lt;strong&gt;It never contained the original request.&lt;/strong&gt; We asked a model&lt;br&gt;
"does this match the request?" without telling it what the request was.&lt;br&gt;
A model asked to judge against information it doesn't have will&lt;br&gt;
hallucinate that information. Ours did, confidently, 245 times' worth.&lt;/p&gt;

&lt;p&gt;Bonus failure: we truncated long documents to 4,000 characters before&lt;br&gt;
review without saying so, and reviewers marked them "thin — cut off&lt;br&gt;
mid-sentence." The cut was ours, not the producer's.&lt;/p&gt;

&lt;h2&gt;
  
  
  How common was it?
&lt;/h2&gt;

&lt;p&gt;We audited all 2,038 reviews on file for concrete terms (product names,&lt;br&gt;
format tokens) that appear in the review but &lt;strong&gt;nowhere in the reviewed&lt;br&gt;
document&lt;/strong&gt;. Result: &lt;strong&gt;4 contaminated reviews — 0.2%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the uncomfortable lesson: a 0.2% hallucination rate produced 470&lt;br&gt;
wasted runs, because nothing ever gave up. Low rate × infinite retries =&lt;br&gt;
unbounded damage. The rate is not the risk; the loop is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixes (all mechanical)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pass the original request into the review prompt.&lt;/strong&gt; If it can't be
extracted, the prompt now says: &lt;em&gt;"do NOT guess the request — say it is
unknown and judge the artifact on its own."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declare truncation.&lt;/strong&gt; "First 4,000 of 8,784 chars — the cut is ours."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject impossible instructions at the review's own exit gate.&lt;/strong&gt;
A review demanding "N lines only" while the production contract requires
600+ chars now fails &lt;em&gt;as a review&lt;/em&gt; and never enters the queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count consecutive contract failures&lt;/strong&gt;, not just reviews, and park the
item for a human after 5 — with the last verdict and fix instruction
attached, so the human can see &lt;em&gt;why&lt;/em&gt; in one glance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each fix ships with a test we deliberately broke to confirm it fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you run agents unattended
&lt;/h2&gt;

&lt;p&gt;The checker that catches broken outputs in this story (empty text, language&lt;br&gt;
leakage, placeholder junk, contract violations) is free on npm:&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/honto-contract" rel="noopener noreferrer"&gt;honto-contract&lt;/a&gt; — it passed&lt;br&gt;
600 downloads last week, so somebody besides us finds this useful now.&lt;/p&gt;

&lt;p&gt;The unattended-operation checklist and three of our watchdog templates are&lt;br&gt;
free (email-gated):&lt;br&gt;
&lt;a href="https://gxcafe.co.jp/harness-kit/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=harness-kit" rel="noopener noreferrer"&gt;Unattended-Operation Kit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The full production set (7 templates — cron registry, silent-zero watch, heartbeat, output contracts: the exact ones in this story) is available on the same page.&lt;br&gt;
(&lt;a href="https://gxcafe.co.jp/harness-kit/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=templates-pro" rel="noopener noreferrer"&gt;https://gxcafe.co.jp/harness-kit/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=templates-pro&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Honest note: we have no customers yet. Everything above is exactly what we&lt;br&gt;
run on ourselves, measured on our own failures.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>devops</category>
    </item>
    <item>
      <title>Architectural Breakdown: We fixed the eval platform we're competing on: a TypeError that crashed thr</title>
      <dc:creator>Muhammad Hammad</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:16:15 +0000</pubDate>
      <link>https://dev.to/agenticstack/architectural-breakdown-we-fixed-the-eval-platform-were-competing-on-a-typeerror-that-crashed-thr-42eg</link>
      <guid>https://dev.to/agenticstack/architectural-breakdown-we-fixed-the-eval-platform-were-competing-on-a-typeerror-that-crashed-thr-42eg</guid>
      <description>&lt;h1&gt;
  
  
  We Fixed the Eval Platform: The TypeError That Took Down Three Benchmark Pipelines
&lt;/h1&gt;

&lt;p&gt;At 3 AM, Sentry lit up with &lt;code&gt;TypeError: Cannot read property 'map' of undefined&lt;/code&gt;. Three benchmark pipelines crashed. Not a memory leak, not a segfault, but a race condition hiding behind a TypeError, turning a high-stakes eval run into chaos. Here is how we resolved it, with no fluff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause: Async Data Meets Blind Faith in .map()
&lt;/h2&gt;

&lt;p&gt;The error trace pointed to &lt;code&gt;evaluator.ts:42&lt;/code&gt;, where &lt;code&gt;.map()&lt;/code&gt; assumed &lt;code&gt;inputData.metrics&lt;/code&gt; would always exist. The junior dev tested with clean data, but in production, &lt;strong&gt;&lt;code&gt;fetchBenchmarkData()&lt;/code&gt; (async) and &lt;code&gt;evaluatePipeline()&lt;/code&gt; (sync) were racing&lt;/strong&gt;. At 100+ RPS, &lt;code&gt;metrics&lt;/code&gt; was often &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Offending Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;computeScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why It Failed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Race Condition&lt;/strong&gt;: &lt;code&gt;inputData&lt;/code&gt; was fetched asynchronously, but &lt;code&gt;evaluatePipeline()&lt;/code&gt; treated it as synchronous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OOM Risk&lt;/strong&gt;: Unbounded &lt;code&gt;.map()&lt;/code&gt; on 10K+ metrics could exhaust 8GB RAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker Starvation&lt;/strong&gt;: No concurrency limits led to thread pool exhaustion.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Fix: Guard Clauses, Bounded Queues, and Pragmatism
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Fail Fast, Fail Loud
&lt;/h3&gt;

&lt;p&gt;Added zero-overhead runtime checks to reject bad data early:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// eval-platform/core/evaluator.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isNullOrUndefined&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/guards&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;evaluatePipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BenchmarkInput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EvaluationResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isNullOrUndefined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EVAL_400: metrics missing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Proceed only if data is valid&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stops &lt;code&gt;TypeError&lt;/code&gt; crashes immediately.&lt;/li&gt;
&lt;li&gt;Cost: 1-2 CPU cycles. Negligible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Chunked Processing for 8GB RAM
&lt;/h3&gt;

&lt;p&gt;Original code processed all metrics at once, causing OOM crashes. Fixed with 100-item chunks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 100 items ≈ 10MB peak memory&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;computeScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memoryUsage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;heapUsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 6GB threshold&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setImmediate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// Yield event loop&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hardware Realities:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6GB Heap Limit&lt;/strong&gt;: Leaves 2GB for the OS and other processes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;setImmediate&lt;/code&gt;&lt;/strong&gt;: Prevents the event loop from choking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Bounded Worker Pool (4 Workers)
&lt;/h3&gt;

&lt;p&gt;Original: Unbounded concurrency caused thread pool meltdown. Fixed with a semaphore-based pool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// eval-platform/utils/worker-pool.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WorkerPool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;activeWorkers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;maxWorkers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxWorkers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxWorkers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxWorkers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpus&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Cap at CPU cores&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeWorkers&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxWorkers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeWorkers&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeWorkers&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;()?.();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_WORKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Safe for 8GB RAM (tested)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WorkerPool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAX_WORKERS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;evaluatePipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why 4 Workers?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8GB RAM&lt;/strong&gt;: 4 workers use ~2GB RAM each, with headroom for garbage collection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU Bound&lt;/strong&gt;: Matches typical 4-core cloud instances.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Immutable Data and Network Timeouts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Mutable &lt;code&gt;inputData&lt;/code&gt; plus async fetches led to race conditions.&lt;br&gt;
&lt;strong&gt;Fix&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// eval-platform/core/data-fetcher.ts&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchBenchmarkData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;benchmarkId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BenchmarkInput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 3s timeout&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/benchmarks/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;benchmarkId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Immutable&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`FETCH_500: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hardware Impact:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3s Timeout&lt;/strong&gt;: Covers 99.9% of network latencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Object.freeze&lt;/code&gt;&lt;/strong&gt;: Zero cost. V8 optimizes frozen objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hardware Profiling: 8GB RAM, No Illusions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Metric&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Before Fix&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;After Fix&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Peak Memory (1K evals)&lt;/td&gt;
&lt;td&gt;7.8GB (OOM crashes)&lt;/td&gt;
&lt;td&gt;5.2GB (stable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU Usage (4 workers)&lt;/td&gt;
&lt;td&gt;100% (thrashing)&lt;/td&gt;
&lt;td&gt;60% (bounded)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error Rate&lt;/td&gt;
&lt;td&gt;12% (&lt;code&gt;TypeError&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;0.01% (guarded)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency (p99)&lt;/td&gt;
&lt;td&gt;12s (unbounded)&lt;/td&gt;
&lt;td&gt;4s (chunked + pooled)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Tuning Notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chunk Size&lt;/strong&gt;: 100 items, balanced for RAM and CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker Pool&lt;/strong&gt;: 4 workers, matches 4-core instances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeouts&lt;/strong&gt;: 3s, because hope is not a strategy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Failure Walkthrough: When Things Still Go Wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scenario 1: 10K Metrics in One Benchmark
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;: OOM crash (7.8GB, OS kills it).&lt;br&gt;
&lt;strong&gt;After&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Chunked processing (100 items/chunk) caps peak memory at 5.2GB.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setImmediate&lt;/code&gt; yields the event loop, preventing starvation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Scenario 2: Network Latency Spike (1s)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;: &lt;code&gt;inputData.metrics&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt;, causing &lt;code&gt;TypeError&lt;/code&gt;.&lt;br&gt;
&lt;strong&gt;After&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;3s timeout aborts stale fetch.&lt;/li&gt;
&lt;li&gt;Immutable &lt;code&gt;inputData&lt;/code&gt; prevents race conditions.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Scenario 3: 200 RPS Burst
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;: 200 workers exhaust the thread pool.&lt;br&gt;
&lt;strong&gt;After&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Worker pool caps at 4, bounding concurrency.&lt;/li&gt;
&lt;li&gt;Queue backpressure enables graceful degradation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Junior vs Senior: The Difference Between Crash and Stability
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Aspect&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Junior (Broken)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Senior (Hardened)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Assumed sync&lt;/td&gt;
&lt;td&gt;Async with guards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unbounded&lt;/td&gt;
&lt;td&gt;Bounded (4 workers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OOM risk&lt;/td&gt;
&lt;td&gt;Chunked (100 items) + 6GB limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error Handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Silent crashes&lt;/td&gt;
&lt;td&gt;Structured errors (&lt;code&gt;EVAL_400&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Integrity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mutable state&lt;/td&gt;
&lt;td&gt;Immutable (&lt;code&gt;Object.freeze&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;We did not reinvent the wheel. We stopped pretending async data would magically synchronize itself. No buzzwords, no hype, just code that does not crash under pressure.&lt;/p&gt;

&lt;p&gt;For a template with these guardrails, see &lt;a href="https://www.shipmvp.tech" rel="noopener noreferrer"&gt;ShipMVP&lt;/a&gt;. It is what we wish we had at 3 AM.&lt;/p&gt;

&lt;p&gt;Now, tell us: what is the worst race condition you have debugged, and how did you fix it?&lt;/p&gt;

</description>
      <category>python</category>
      <category>react</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>OzBrain's Shared Memory Architecture: How Multi-Agent Teams Avoid Re-Explaining Context Across Sessions</title>
      <dc:creator>mech.app</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:07:24 +0000</pubDate>
      <link>https://dev.to/mech_app_ai/ozbrains-shared-memory-architecture-how-multi-agent-teams-avoid-re-explaining-context-across-1c19</link>
      <guid>https://dev.to/mech_app_ai/ozbrains-shared-memory-architecture-how-multi-agent-teams-avoid-re-explaining-context-across-1c19</guid>
      <description>&lt;p&gt;When you run multiple agents across Claude, ChatGPT, and Cursor, each one starts from scratch unless you manually paste context into every session. OzBrain solves this by exposing a shared knowledge substrate that agents read and write through the Model Context Protocol (MCP). The system routes context so agents see only what they need, and teams avoid explaining the same facts to every new agent instance.&lt;/p&gt;

&lt;p&gt;The Show HN post drew 85 points and 50 comments because the problem is real: production multi-agent workflows break down when context lives in isolated chat histories or scattered documents. OzBrain's architecture treats knowledge as a first-class resource with explicit scoping, indexing, and conflict resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage Layer and Scope Boundaries
&lt;/h2&gt;

&lt;p&gt;OzBrain organizes knowledge into &lt;strong&gt;brains&lt;/strong&gt;, which are either personal or shared. Each brain holds structured knowledge units that agents query through the MCP connector. The system decides scope at write time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Personal brains&lt;/strong&gt; store user-specific preferences, writing style, and private project state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared brains&lt;/strong&gt; hold team-wide facts like client contacts, project decisions, and open threads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an agent writes to OzBrain, it specifies the target brain. The MCP connector enforces access control: agents can read from any brain the user has joined, but write permissions depend on the brain's sharing policy. This prevents accidental leakage of personal context into team memory.&lt;/p&gt;

&lt;p&gt;The storage layer tags each knowledge unit with metadata: creation timestamp, last update, and a freshness indicator (fresh, aging, stale). Agents use these tags to decide whether to trust the stored fact or re-query the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Indexing Strategy and Query Routing
&lt;/h2&gt;

&lt;p&gt;OzBrain does not load the entire knowledge graph into every prompt. Instead, it maintains a &lt;strong&gt;routing index&lt;/strong&gt; that maps topics to knowledge units. When an agent queries for "client contacts," the index returns pointers to relevant units without pulling in unrelated project state.&lt;/p&gt;

&lt;p&gt;The routing index uses a simple keyword and topic model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each knowledge unit declares its topic (e.g., "clients/meridian", "voice", "projects/q3-launch").&lt;/li&gt;
&lt;li&gt;The index builds a reverse lookup from topic to unit ID.&lt;/li&gt;
&lt;li&gt;Agents send a topic query through the MCP connector, which returns a ranked list of unit IDs.&lt;/li&gt;
&lt;li&gt;The agent fetches only the top-ranked units, keeping the prompt under token budget.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach trades precision for speed. The index does not use embeddings or semantic search, so agents must know the right topic label. In practice, this works because teams establish naming conventions early (e.g., "clients/", "projects/", "preferences/").&lt;/p&gt;

&lt;h2&gt;
  
  
  Conflict Resolution When Multiple Agents Write
&lt;/h2&gt;

&lt;p&gt;When two agents update the same knowledge unit, OzBrain uses last-write-wins with a conflict flag. The system does not merge changes automatically. Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agent A writes a new version of "projects/q3-launch" with status "delayed."&lt;/li&gt;
&lt;li&gt;Agent B writes a conflicting version with status "on track" 30 seconds later.&lt;/li&gt;
&lt;li&gt;OzBrain stores Agent B's version as the current state but flags the unit as "conflicted."&lt;/li&gt;
&lt;li&gt;The next agent to read "projects/q3-launch" sees the conflict flag and can surface it to the user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a deliberate trade-off. Automatic merging requires semantic understanding of the conflict, which OzBrain does not attempt. The conflict flag ensures that contradictions do not silently propagate through the team's shared memory.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Conflict Strategy&lt;/th&gt;
&lt;th&gt;Precision&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Failure Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Last-write-wins&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;td&gt;Silent overwrites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual merge&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;User fatigue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OzBrain (flag + LWW)&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;td&gt;Requires agent or user to check flags&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Failure Modes and Staleness
&lt;/h2&gt;

&lt;p&gt;Shared memory introduces a new failure mode: &lt;strong&gt;stale context&lt;/strong&gt;. If a knowledge unit says "client prefers email" but the client switched to Slack last week, agents will make incorrect assumptions until someone updates the unit.&lt;/p&gt;

&lt;p&gt;OzBrain mitigates this with freshness tags. When an agent reads a unit marked "aging," it can prompt the user to confirm the fact before acting. The system does not auto-expire knowledge because some facts (e.g., "brand voice guidelines") remain valid for months.&lt;/p&gt;

&lt;p&gt;The more dangerous failure mode is &lt;strong&gt;contradictory context&lt;/strong&gt;. If an agent's working memory (from the current session) conflicts with OzBrain's shared memory, the agent must decide which to trust. OzBrain does not provide a resolution mechanism. Agents typically trust their working memory for session-specific facts and defer to shared memory for long-lived state.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Connector Implementation
&lt;/h2&gt;

&lt;p&gt;OzBrain exposes its API through an MCP server at &lt;code&gt;https://ozbrain.com/api/mcp&lt;/code&gt;. Agents connect by adding the server to their MCP configuration. The connector supports four operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;list_brains&lt;/code&gt;: Returns all brains the user can access.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;query_brain(brain_id, topic)&lt;/code&gt;: Returns knowledge units matching the topic.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;write_unit(brain_id, topic, content)&lt;/code&gt;: Creates or updates a knowledge unit.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;read_unit(brain_id, unit_id)&lt;/code&gt;: Fetches a specific unit by ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a minimal example of an agent querying for client context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://ozbrain.com/api/mcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# List available brains
&lt;/span&gt;&lt;span class="n"&gt;brains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list_brains&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;team_brain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;brains&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team-shared&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Query for client contacts
&lt;/span&gt;&lt;span class="n"&gt;units&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_brain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brain_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;team_brain&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clients/meridian&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# Fetch the top-ranked unit
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;contact_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read_unit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brain_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;team_brain&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unit_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP connector handles authentication via email-based login codes. When a user first connects, OzBrain sends a one-time code to their email. The agent exchanges the code for a session token, which it stores for future requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment Shape and Observability
&lt;/h2&gt;

&lt;p&gt;OzBrain runs as a hosted service. Users do not self-host the storage layer or indexing infrastructure. This simplifies deployment but introduces a dependency on OzBrain's availability. If the MCP endpoint goes down, agents lose access to shared memory and fall back to session-only context.&lt;/p&gt;

&lt;p&gt;The system does not expose detailed observability hooks. Agents cannot trace which knowledge units were queried or how long the index lookup took. This makes debugging slow queries difficult. Teams must rely on OzBrain's internal logging, which is not surfaced to users.&lt;/p&gt;

&lt;p&gt;For teams that need audit trails, the lack of query logs is a blocker. You cannot reconstruct which agent read which fact at what time, so compliance workflows that require provenance tracking will struggle.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use OzBrain
&lt;/h2&gt;

&lt;p&gt;OzBrain fits teams that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run multiple agents (Claude, ChatGPT, Cursor) and need consistent context across tools.&lt;/li&gt;
&lt;li&gt;Have established naming conventions for topics and can train agents to query the right labels.&lt;/li&gt;
&lt;li&gt;Accept last-write-wins conflict resolution and are willing to manually resolve contradictions.&lt;/li&gt;
&lt;li&gt;Trust a hosted service for knowledge storage and do not require self-hosted deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid OzBrain if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need semantic search or embeddings-based retrieval. The keyword index is too brittle for open-ended queries.&lt;/li&gt;
&lt;li&gt;Your workflow requires automatic conflict merging. The conflict flag is a signal, not a solution.&lt;/li&gt;
&lt;li&gt;You need detailed query logs or observability into agent memory access.&lt;/li&gt;
&lt;li&gt;You require on-premises deployment or air-gapped operation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical Verdict
&lt;/h2&gt;

&lt;p&gt;OzBrain solves the context duplication problem with a straightforward storage and indexing layer. The MCP connector makes it easy to wire into existing agent workflows, and the scoping model (personal vs. shared brains) prevents accidental leakage. The routing index keeps prompts small by fetching only relevant knowledge units.&lt;/p&gt;

&lt;p&gt;The trade-offs are clear: last-write-wins conflict resolution, no semantic search, and reliance on a hosted service. For teams that can live with these constraints, OzBrain removes the friction of re-explaining context to every new agent session. For teams that need richer conflict resolution or self-hosted deployment, the architecture is too opinionated.&lt;/p&gt;

&lt;p&gt;The freshness tagging is a smart middle ground between auto-expiration (which breaks long-lived facts) and no expiration (which lets stale data accumulate). The conflict flag is less satisfying because it pushes resolution back to the user or agent, but automatic merging would require semantic understanding that OzBrain does not attempt.&lt;/p&gt;

&lt;p&gt;If your multi-agent workflow is breaking down because agents cannot share context, OzBrain is worth testing. If you need fine-grained control over conflict resolution or query observability, you will hit the ceiling quickly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Source Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ozbrain.com" rel="noopener noreferrer"&gt;OzBrain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=49394827" rel="noopener noreferrer"&gt;Hacker News Discussion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Architecting a background-service-based sound manager that survives Android's Doze mode</title>
      <dc:creator>Haseeb</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:07:12 +0000</pubDate>
      <link>https://dev.to/haseebthedev0/architecting-a-background-service-based-sound-manager-that-survives-androids-doze-mode-1o1e</link>
      <guid>https://dev.to/haseebthedev0/architecting-a-background-service-based-sound-manager-that-survives-androids-doze-mode-1o1e</guid>
      <description>&lt;p&gt;It was the final ten minutes of a high-stakes client presentation. I was mid-sentence, explaining a complex system migration, when my phone erupted with a loud, aggressive ringtone. The room went silent, but my phone did not. I scrambled to silence it, accidentally hitting the volume buttons while fumbling with the screen. That moment of pure, unadulterated embarrassment followed me for days. It was not the first time this had happened, but it was the time I decided I had finally had enough of relying on my own memory to toggle sound profiles before entering sensitive environments.&lt;/p&gt;

&lt;p&gt;Most of us live in a state of perpetual concern regarding our devices. We walk into movie theaters, attend religious services, or sit through medical consultations, constantly checking our pockets to ensure we have toggled the mute switch. If we forget, we face the social friction of a disruption. The existing solutions were either too manual—requiring a conscious effort I rarely possessed in the moment—or too intrusive, demanding constant location permissions and draining the battery to perform simple state changes. I wanted something that functioned as a set-and-forget background utility. I needed a system that understood the context of my environment without requiring me to interact with an interface every time my routine shifted.&lt;/p&gt;

&lt;p&gt;To build this, I had to architect a background service that could survive the aggressive power-management constraints of modern Android, specifically Doze mode. The primary challenge was ensuring that my sound-toggling logic fired precisely when a rule was triggered, even if the device had been sitting idle for hours. I initially experimented with a standard &lt;code&gt;Service&lt;/code&gt;, but Android’s lifecycle management quickly killed it to save resources. I shifted to using a &lt;code&gt;ForegroundService&lt;/code&gt; with a persistent notification, which is the standard approach for long-running tasks, but that only solved the visibility part. The real hurdle was the timing accuracy required for events like prayer times or calendar-based meetings.&lt;/p&gt;

&lt;p&gt;I eventually realized that relying solely on a service was a mistake. I needed to leverage &lt;code&gt;AlarmManager&lt;/code&gt; with &lt;code&gt;setExactAndAllowWhileIdle&lt;/code&gt;. This allows the system to wake the device from Doze mode to fire a broadcast, which I then use to trigger the &lt;code&gt;AudioManager&lt;/code&gt; state changes. The architecture looks roughly like this:&lt;/p&gt;

&lt;p&gt;kotlin&lt;br&gt;
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager&lt;br&gt;
val intent = Intent(context, MuffleBroadcastReceiver::class.java)&lt;br&gt;
val pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_IMMUTABLE)&lt;/p&gt;

&lt;p&gt;alarmManager.setExactAndAllowWhileIdle(&lt;br&gt;
    AlarmManager.RTC_WAKEUP,&lt;br&gt;
    triggerTimeInMillis,&lt;br&gt;
    pendingIntent&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;By decoupling the scheduling from the execution logic, I ensured that even if the OS aggressively restricts background processes, the kernel still respects the alarm trigger. The &lt;code&gt;MuffleBroadcastReceiver&lt;/code&gt; then handles the heavy lifting, checking the priority of the current routine against any overlapping rules before calling &lt;code&gt;audioManager.setRingerMode&lt;/code&gt; to toggle between silent, vibrate, or Do Not Disturb. This separation of concerns—scheduling via &lt;code&gt;AlarmManager&lt;/code&gt; and execution via &lt;code&gt;BroadcastReceiver&lt;/code&gt;—is what keeps the system stable across different manufacturer implementations of the Android OS.&lt;/p&gt;

&lt;p&gt;What surprised me most during development was the volatility of the &lt;code&gt;Do Not Disturb&lt;/code&gt; (DND) API. I initially assumed that simply calling the &lt;code&gt;setRingerMode&lt;/code&gt; method would be sufficient to enforce silence. I was wrong. On many devices, specifically those from manufacturers with heavy custom UI skins, the DND access permissions are revoked or reset after major system updates. I spent days debugging why my application would stop silencing the phone despite the service running perfectly. It turned out that the &lt;code&gt;NotificationManager.isNotificationPolicyAccessGranted&lt;/code&gt; check needed to be far more frequent than I anticipated. I had to implement a listener that re-verifies this permission every time the service starts, rather than just once during the initial setup. Relying on a one-time permission grant was an architectural oversight that nearly crippled the app's reliability for users on newer API levels.&lt;/p&gt;

&lt;p&gt;Another edge case that caught me off guard was how &lt;code&gt;AlarmManager&lt;/code&gt; behaves when the system time changes, such as during a Daylight Savings Time shift. My logic was originally tied to absolute timestamps in milliseconds. When the system clock adjusted, my scheduled routines shifted by an hour, causing them to trigger at the wrong time. I had to pivot to storing local time representations and re-calculating the trigger time whenever a &lt;code&gt;TIME_SET&lt;/code&gt; or &lt;code&gt;TIMEZONE_CHANGED&lt;/code&gt; broadcast was received. It was a tedious fix, but it taught me that you cannot treat time as a static constant on a mobile device. If I were to start over, I would build a much more robust abstraction layer for time-based triggers that explicitly handles these system-level shifts from the beginning, rather than patching them as bugs after the fact.&lt;/p&gt;

&lt;p&gt;For any developer building automation tools on Android, the biggest lesson is to stop fighting the OS power-management systems and start working within their constraints. Doze mode is not a bug to be bypassed; it is a feature that keeps the user's phone alive for multiple days. If your app requires background execution, you must accept that you will be throttled. Instead of trying to keep a background service running 24/7, design your application to be event-driven. Use &lt;code&gt;AlarmManager&lt;/code&gt; for specific time-based tasks and &lt;code&gt;WorkManager&lt;/code&gt; for periodic synchronization or maintenance. If you try to force a persistent, always-active background process, you will eventually find your app getting killed by the system’s memory management, and you will lose the user's trust.&lt;/p&gt;

&lt;p&gt;Always prioritize the user's battery life. If your background utility consumes significant energy, users will uninstall it, regardless of how useful the features are. I built Muffle with these exact principles in mind, focusing on minimal resource footprint by keeping the logic local and avoiding unnecessary network calls. By keeping the app fully offline, I also ensured that privacy and performance remained at the core of the experience. You can see how I implemented these constraints by checking out the project at &lt;a href="https://play.google.com/store/apps/details?id=com.muffle.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.muffle.app&lt;/a&gt;. Building for Android is as much about managing system resources as it is about writing clean code, and finding that balance is what makes an app feel like a native extension of the OS.&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>mobiledev</category>
      <category>androiddev</category>
    </item>
    <item>
      <title>Construyendo un recomendador de emparejamiento de expertos</title>
      <dc:creator>Franchesco Romero</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:03:34 +0000</pubDate>
      <link>https://dev.to/aws-builders/construyendo-un-recomendador-de-emparejamiento-de-expertos-57h2</link>
      <guid>https://dev.to/aws-builders/construyendo-un-recomendador-de-emparejamiento-de-expertos-57h2</guid>
      <description>&lt;h2&gt;
  
  
  La forma del problema
&lt;/h2&gt;

&lt;p&gt;Un directorio es una superficie: el miembro lo abre y adivina. &lt;br&gt;
Un recomendador es una superficie de empujar: el sistema propone y tiene que justificarse. La justificación es la parte difícil, y es donde vive la estadística.&lt;/p&gt;

&lt;p&gt;Tres restricciones hicieron esto distinto de un recomendador de contenido:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;El item es una persona con capacidad finita.&lt;/strong&gt; Un hilo se le puede recomendar a diez mil personas. Un experto no.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Una mala recomendación es cara de los dos lados.&lt;/strong&gt; Quien pide desperdicia una petición, el experto desperdicia una hora, y los dos aprenden a ignorar la superficie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La afirmación tiene que ser checable.&lt;/strong&gt; "Quizá te guste este hilo" no necesita evidencia. "Esta persona está un nivel adelante de ti en diseño de sistemas" sí.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Recuperación: híbrida, fusionada con RRF
&lt;/h2&gt;

&lt;p&gt;Tres recuperadores independientes sobre el conjunto de expertos elegibles, fusionados con Reciprocal Rank Fusion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rrf_fuse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ranked_lists&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fusiona listas de ids rankeadas. El score depende solo del rank, nunca
    de la escala propia del recuperador, que es el punto: la similitud coseno y
    un conteo de hilos resueltos no son números comparables.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;fused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lst&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked_lists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lst&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;fused&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fused&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fused&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RRF es la primitiva correcta aquí por una razón que vale la pena decir: los recuperadores emiten cantidades incomparables. Uno regresa un coseno en &lt;code&gt;[-1, 1]&lt;/code&gt;, uno regresa un conteo entero de hilos resueltos, uno regresa un delta de nivel de escalera. Normalizarlos a una escala común requiere supuestos sobre sus distribuciones que nadie tiene a este volumen de datos. &lt;br&gt;
RRF descarta las magnitudes y se queda solo con el orden, que es exactamente la información que sobrevive a una muestra chica.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;k = 60&lt;/code&gt; es la constante estándar de la formulación original de Cormack et al.&lt;br&gt;
Aplana la cabeza: la diferencia entre el rank 1 y el rank 2 es&lt;br&gt;
&lt;code&gt;1/61 - 1/62 ≈ 0.00026&lt;/code&gt;, así que un recuperador no puede dominar por estar confiado, solo por estar consistentemente temprano a lo largo de las listas.&lt;/p&gt;
&lt;h2&gt;
  
  
  Scoring: un compuesto ponderado, con los pesos como datos
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SCORE_WEIGHTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compass_gap_fit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# explícito y direccional
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic_fit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# coseno del embedding de perfil
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skill_overlap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# tecnologías compartidas, ponderadas por rareza
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capacity_fit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# también una compuerta dura
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expert_quality&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# rating, aceptación, experiencia que satura
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;specialty_match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# burda, pero muy poblada
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fairness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# penalización de exposición amortiguada por log
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Dos componentes valen la pena desempacar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;expert_quality&lt;/code&gt; satura.&lt;/strong&gt; Un experto sin historial puntúa 0.5, no 0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;expert_quality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;avg_rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proposed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;proposed&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;                      &lt;span class="c1"&gt;# prior neutral, no cero
&lt;/span&gt;    &lt;span class="n"&gt;rating_part&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;avg_rating&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;
    &lt;span class="n"&gt;acceptance&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;proposed&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;proposed&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;
    &lt;span class="n"&gt;experience&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rating_part&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;acceptance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;experience&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La saturación exponencial sobre &lt;code&gt;experience&lt;/code&gt; codifica que la diferencia entre 0 y 5 sesiones completadas es grande y la diferencia entre 40 y 45 es ruido.&lt;br&gt;
Un término lineal habría hecho inalcanzables a los veteranos. El prior de 0.5 para los no probados es la decisión de &lt;em&gt;cold start&lt;/em&gt; que deja crecer el pool de expertos más allá de quien haya ido primero; sin él, el sistema es un loop de &lt;em&gt;rico se hace más rico&lt;/em&gt; por construcción.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;fairness&lt;/code&gt; es exposición amortiguada por log.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fairness_factor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;times_recommended&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;times_recommended&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ln(e + n)&lt;/code&gt; da exactamente 1.0 en &lt;code&gt;n = 0&lt;/code&gt; y decae lento. Una penalización lineal habría hecho irrecomendable a un buen experto después de un puñado de ciclos.&lt;/p&gt;

&lt;h2&gt;
  
  
  La parte que importa: esto es un problema de asignación
&lt;/h2&gt;

&lt;p&gt;El instinto es computar un top-N por cada quien pide. Ese instinto está mal, y el modo de falla no es sutil.&lt;/p&gt;

&lt;p&gt;Si cada quien pide escoge de forma independiente a su mejor experto, las mismas tres personas más fuertes juntan todas las peticiones. Son las que tienen los mejores ratings y el historial más profundo, así que ganan cada ranking, y dejan de contestar en un mes. El recomendador entonces destruye el recurso que existe para asignar.&lt;/p&gt;

&lt;p&gt;Así que los pares se puntúan, y luego se asignan de forma global bajo una restricción de capacidad por experto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per_requester&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Asignación global greedy. Ordenada por score, cada par consume una unidad
    de la capacidad de su experto.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;assigned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pair&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expert_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;assigned&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requester_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;per_requester&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expert_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;assigned&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requester_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esta es la aproximación greedy a un matching bipartito con restricción de grado. A esta escala (cientos de pares) la solución óptima vía &lt;code&gt;scipy.optimize.linear_sum_assignment&lt;/code&gt; y la greedy difieren por ruido, y la versión greedy tiene una propiedad que la óptima no: es inspeccionable en un dry run, línea por línea, en orden de score. Cuando un operador pregunta "¿por qué esta persona obtuvo ese experto?", la respuesta es una sola pasada hacia abajo por una lista ordenada.&lt;/p&gt;

&lt;p&gt;A quienes piden que la asignación no puede colocar no se les tira. Por construcción son aquellos cuyos mejores expertos están llenos, lo que los hace el insumo exacto para el clustering uno-a-muchos: agrúpalos por celda de escalera y propón una sola sesión.&lt;/p&gt;

&lt;h2&gt;
  
  
  La capa de datos, y la estadística que la hace defendible
&lt;/h2&gt;

&lt;p&gt;La segunda capa responde "qué debería aprender después, y cuánto vale".&lt;br&gt;
Compara la mediana de datos ponderada por fuente de los puntos de datos que reportan una habilidad contra los que no.&lt;/p&gt;

&lt;p&gt;Esa oración contiene tres maneras de engañar a alguien. Las tres necesitaron una compuerta.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Ponderación por fuente con decaimiento exponencial por recencia
&lt;/h3&gt;

&lt;p&gt;No todos los puntos de datos merecen voto igual. Cada fuente carga un peso de confianza y una corrección de sesgo, y cada punto decae con la edad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bias_correction_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;POWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPOCH&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;sdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scraped_at&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;halflife&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una vida media de 365 días significa que una publicación de dos años todavía cuenta, a un cuarto del peso de una fresca. El &lt;code&gt;LEFT JOIN&lt;/code&gt; sobre la tabla de fuentes es deliberado: un punto de datos cuya fila de fuente nunca se registró cuenta en el default neutral de 0.50 en lugar de desvanecerse, porque tirar datos en silencio es peor que ponderarlos de forma conservadora.&lt;/p&gt;

&lt;p&gt;El agregado es una &lt;strong&gt;mediana ponderada&lt;/strong&gt;, no una media ponderada. Las&lt;br&gt;
distribuciones de datos están sesgadas a la derecha y la cola es donde viven los errores de scraping; una sola cifra mal parseada mueve una media y no mueve una mediana.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;weighted_median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                       &lt;span class="c1"&gt;# todos los pesos en cero: degrada a mediana simple
&lt;/span&gt;        &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
                &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;half&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;half&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Estratificación, porque el número ingenuo mide antigüedad
&lt;/h3&gt;

&lt;p&gt;Este es el confusor que hace inútiles a la mayoría de las afirmaciones de "la habilidad X paga Y% más". La gente senior sabe más herramientas. Compara a todos los que reportan Kubernetes contra todos los que no, y una parte grande del delta es nada más antigüedad filtrándose por la comparación.&lt;/p&gt;

&lt;p&gt;Cada comparación por lo tanto pasa &lt;strong&gt;dentro&lt;/strong&gt; de un estrato &lt;code&gt;(rol, seniority, país, bucket de experiencia)&lt;/code&gt;. Los buckets son burdos a propósito (&lt;code&gt;0-2&lt;/code&gt;, &lt;code&gt;3-5&lt;/code&gt;, &lt;code&gt;6-9&lt;/code&gt;, &lt;code&gt;10+&lt;/code&gt;): estratos más finos matan de hambre a la muestra y el intervalo explota. Una habilidad cuyo efecto desaparece una vez estratificada se tira, no se reporta.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Un intervalo por bootstrap, no un estimado puntual
&lt;/h3&gt;

&lt;p&gt;Un estimado puntual se lee como una promesa. El bootstrap por percentil re muestrea las dos cohortes con reemplazo, recomputa las medianas ponderadas, y toma los percentiles 5/95 de la distribución de deltas resultante:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bootstrap_ci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;with_vals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;with_w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;without_vals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;without_w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SEED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# determinista: los mismos insumos deben
&lt;/span&gt;    &lt;span class="n"&gt;deltas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;                          &lt;span class="c1"&gt;# producir el mismo intervalo en cada corrida
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;with_vals&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;with_vals&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;without_vals&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;without_vals&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;med_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;weighted_median&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;with_vals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;with_w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;med_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;weighted_median&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;without_vals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;without_w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;med_b&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;deltas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;med_a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;med_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;med_b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;deltas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deltas&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deltas&lt;/span&gt;&lt;span class="p"&gt;)))]&lt;/span&gt;
    &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deltas&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deltas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deltas&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El bootstrap es la herramienta correcta porque la distribución muestral de una &lt;em&gt;mediana ponderada de una distribución sesgada&lt;/em&gt; no tiene una forma cerrada limpia. El re muestreo esquiva la derivación por completo.&lt;/p&gt;

&lt;p&gt;El RNG sembrado importa más de lo que parece. Un operador refrescando un dry run de admin no debe ver el número bambolearse; un intervalo de confianza que cambia al recargar es indistinguible de un bug.&lt;/p&gt;

&lt;p&gt;Una fila se surge solo si las tres se cumplen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;significant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ci_low&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;                  &lt;span class="c1"&gt;# el intervalo se queda de un lado del cero
&lt;/span&gt;               &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;premium&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;min_pct&lt;/span&gt;        &lt;span class="c1"&gt;# bastante grande para valer el tiempo de una persona
&lt;/span&gt;               &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;premium&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;claim_cap_pct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# no un outlier absurdo
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Las filas no significativas de todos modos se computan y se guardan. La tabla de admin muestra qué se rechazó y por qué, porque un número que el sistema se negó a usar es tan interesante como uno que usó.&lt;/p&gt;

&lt;h2&gt;
  
  
  La medición que reencuadró el proyecto
&lt;/h2&gt;

&lt;p&gt;Todo lo de arriba estaba en verde en CI. Luego la primera dry run contra producción:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;requesters&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;82  experts=1  scored_pairs=82  affinity=3  office_hours=0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1 experto de 51.&lt;/strong&gt; La causa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;requester_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;requesters&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;eligible_experts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;experts&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open_load&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;requester_ids&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;-- esto
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La intención era "no emparejes a alguien consigo mismo". La implementación era "excluye a cualquier experto que sea también un candidato a pedir", y casi todos los expertos lo son, porque no tienen ninguna petición abierta propia.&lt;/p&gt;

&lt;p&gt;Medido:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Roles activos elegibles como experto:                51
...que se apuntaron:                                 51
...bajo el tope de carga:                            51
...excluidos por ser también quienes piden:             50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El único sobreviviente era la única persona que resultó estar a media&lt;br&gt;
interacción, y las tres propuestas apuntaban a ella. El sistema había&lt;br&gt;
encontrado el modo de falla de burnout por su cuenta, en la primera corrida, a través de una línea pensada para prevenir un problema completamente distinto.&lt;/p&gt;

&lt;p&gt;La guarda de auto emparejamiento ya existía por par, que es donde va.&lt;/p&gt;

&lt;p&gt;Dos hallazgos más de la misma corrida:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Las cards podían salir sin razón.&lt;/strong&gt; Solo 4 miembros tenían una colocación de escalera de competencias, así que el componente más pesado casi siempre era 0 y la señal sobreviviente era un coseno de embedding que nadie puede leer. Una propuesta tenía una lista de razones vacía. Una card que no puede decir por qué es peor que ninguna card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El badge de tendencia disparaba con todo.&lt;/strong&gt; La ingesta era reciente, así que casi cada publicación caía en la ventana de 30 días y la línea base de 90 días era una o dos filas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;golang   229 publicaciones  trend=2.00
go       155 publicaciones  trend=462.00
python   109 publicaciones  trend=62.40
aws       43 publicaciones  trend=61.50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una razón necesita un denominador que valga la pena dividir. El arreglo es un conteo mínimo de línea base antes de que se afirme una tendencia siquiera, y una cota sobre la razón.&lt;/p&gt;

&lt;h2&gt;
  
  
  La lección de frecuencia inversa, aprendida tres veces
&lt;/h2&gt;

&lt;p&gt;Esta es la parte con el mayor valor de transferencia, porque la misma idea estadística tuvo que aplicarse en tres niveles distintos y cada nivel se veía bien hasta que se inspeccionó.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nivel 1: lift, en el minero de adyacencia
&lt;/h3&gt;

&lt;p&gt;La adyacencia de habilidades es minería clásica de reglas de asociación sobre habilidades co-ocurrentes en publicaciones de vacantes. Para una regla&lt;br&gt;
&lt;code&gt;A -&amp;gt; B&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;soporte&lt;/strong&gt; = conteo de transacciones que contienen las dos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;confianza&lt;/strong&gt; = &lt;code&gt;P(B | A)&lt;/code&gt; = soporte / conteo(A)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lift&lt;/strong&gt; = &lt;code&gt;confianza / P(B)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La confianza sola es inútil, y la razón es instructiva. Linux co-ocurre con todo, así que &lt;code&gt;P(Linux | lo que sea)&lt;/code&gt; es alta y cada regla apunta a Linux. El lift divide por la tasa base: si saber A no sube la probabilidad de B por encima del azar, el lift es 1 y la regla no carga información.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;lift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;singles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;consequent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lift&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_lift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# 1.15
&lt;/span&gt;    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reglas que sobrevivieron sobre datos reales, ordenadas por &lt;code&gt;confianza * lift&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;laravel        -&amp;gt; php            conf=0.73  lift=11.33  support=8
terraform      -&amp;gt; kubernetes     conf=0.75  lift=9.25   support=9
rails          -&amp;gt; ruby           conf=0.78  lift=7.94   support=7
react native   -&amp;gt; react          conf=1.00  lift=5.48   support=7
gcp            -&amp;gt; aws            conf=0.73  lift=6.15   support=16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esas se leen bien para un humano, que es la única validación disponible a este tamaño de muestra.&lt;/p&gt;

&lt;p&gt;Una trampa que vale la pena registrar: las primeras pruebas unitarias de esto eran degeneradas. Un corpus donde el antecedente aparece en &lt;em&gt;cada&lt;/em&gt; transacción tiene lift 1.0 por construcción, así que nada puede ser significativo jamás.&lt;/p&gt;

&lt;p&gt;Los fixtures tuvieron que reescribirse para incluir publicaciones que no contuvieran ningún lado de la regla. Un corpus de mercado es diverso; un corpus de prueba tiene que serlo también, o no prueba nada.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nivel 2: frecuencia de documento, en la señal de tema
&lt;/h3&gt;

&lt;p&gt;El riel salió y casi cada card decía:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Comparten temas: introduccion-plataforma.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;La distribución de frecuencia de tags lo explica:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;talento-tecnologico        27 hilos   50.0%
introduccion-profesional   17 hilos   31.5%
introduccion-plataforma    14 hilos   25.9%
espacio-relajacion          4 hilos    7.4%
servidores-caseros          4 hilos    7.4%
ai-local-vs-nube            2 hilos    3.7%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tres tags cubren la mitad de un corpus de 54 hilos, luego un acantilado a 7%.&lt;/p&gt;

&lt;p&gt;Este es el problema de Linux otra vez, un nivel arriba: un tag sobre la mitad del corpus no carga información sobre un par. Los tags por encima de 20% de frecuencia de documento se tiraron del scoring y del copy.&lt;/p&gt;

&lt;p&gt;Ese arreglo hizo el copy menos vergonzoso sin hacerlo significar nada, lo que lleva al tercer nivel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nivel 3: el vocabulario era la ontología equivocada
&lt;/h3&gt;

&lt;p&gt;Existían nueve tags en total. &lt;strong&gt;Ninguno de ellos era una tecnología.&lt;/strong&gt; &lt;br&gt;
Eran secciones del foro: un tablero de anuncios, dos áreas de introducción, un espacio fuera de tema. Ningún umbral de frecuencia puede rescatar una señal que está midiendo lo equivocado. Compartir "introducciones" significa que las dos personas se presentaron.&lt;/p&gt;

&lt;p&gt;La señal técnica que sí existía era el &lt;code&gt;tech_stack&lt;/code&gt; del perfil:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python 11 · aws 8 · docker 7 · node.js 7 · typescript 7
git 6 · react 6 · fastapi 5 · kubernetes 3 · terraform 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flaca (20 de 83 miembros) pero real, y nombra cosas que un experto puede enseñar. El componente de tema se reemplazó por completo, normalizado por el menor de los dos stacks para que un stack de 30 items no pueda diluir un buen match de 3-de-4, y extendido a través de las reglas de adyacencia minadas para que quien pide sobre Python conecte con un experto sobre FastAPI con la regla como la justificación declarada.&lt;/p&gt;

&lt;p&gt;Y luego, predeciblemente, el nivel 3 tuvo su propio nivel:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;También trabaja con python.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Python está en 11 de 20 stacks. El mismo problema, tercera recursión. La resolución esta vez &lt;em&gt;no&lt;/em&gt; fue un filtro duro, porque a diferencia de una sección de foro, Python es una habilidad enseñable real y ponerla en cero descarta traslape genuino. &lt;/p&gt;

&lt;p&gt;Las habilidades ubicuas se &lt;strong&gt;bajan de peso a un quinto&lt;/strong&gt; en lugar de tirarse, el copy nombra las habilidades más raras&lt;br&gt;
primero, y un stack compartido solo justifica una card cuando al menos una habilidad compartida es poco común:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;weight_of&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;common_skills&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una habilidad compartida rara ahora le gana a una ubicua 1.0 a 0.2, que es lo que pone a Terraform encima de Python en el ranking en lugar de debajo por accidente de quién lista más tecnologías.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La generalización: cada vez que una señal es un traslape de conjuntos, pregúntate cuál es la tasa base de cada elemento antes de dejar hablar al traslape.&lt;/strong&gt; El lift, el IDF, y este bajado de peso de habilidades son la misma corrección con tres nombres distintos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Afinado de pesos como learning to rank offline
&lt;/h2&gt;

&lt;p&gt;Los pesos compuestos se guardan en config, no en código, y hay una dry run que propone un rebalanceo a partir de los resultados calificados de los propios miembros:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;correlations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;safe_corr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tres decisiones deliberadas alrededor de ella:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La clase positiva es una sesión agendada, no un click.&lt;/strong&gt; Un click en un hilo es barato; convertir una propuesta en una sesión de verdad agendada no. &lt;/p&gt;

&lt;p&gt;Optimizar por clicks afinaría el ranking hacia la curiosidad en lugar de hacia sesiones que pasan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La propuesta es una mezcla conservadora&lt;/strong&gt;, nunca el vector crudo derivado de la correlación:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;blended&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BLEND&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;corr_w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;BLEND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con &lt;code&gt;BLEND = 0.5&lt;/code&gt; y un piso de 30 muestras, una semana flaca no puede mover el ranking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nada se aplica automáticamente.&lt;/strong&gt; Un operador previsualiza y aplica de forma explícita. Auto aplicar una correlación computada sobre decenas de muestras es como un ranker oscila.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trampas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trampa 1: la restricción de unicidad que creó duplicados.&lt;/strong&gt; Las filas son únicas por &lt;code&gt;(requester, expert, cycle_key)&lt;/code&gt;, lo cual es correcto por ciclo y mal a través de ellos. El cron semanal escribió un segundo ciclo encima del primero y el riel de inmediato se dobló:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;riel para &amp;lt;miembro&amp;gt;:
   &amp;lt;experto-a&amp;gt;  0.3997
   &amp;lt;experto-a&amp;gt;  0.3871   &amp;lt;--
   &amp;lt;experto-b&amp;gt;  0.3589
   &amp;lt;experto-b&amp;gt;  0.3462   &amp;lt;--
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arreglado en dos lugares, porque cualquiera solo deja un hoyo: la lectura se colapsa con &lt;code&gt;DISTINCT ON (expert_id)&lt;/code&gt; quedándose con la mejor fila, y un ciclo nuevo reemplaza las propuestas sin tocar de los viejos para que la tabla no pueda crecer una card por persona por semana para siempre. Solo se tiran las filas no mostradas; cualquier cosa que un miembro vio es historial de embudo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trampa 2: el CTA que no hacía nada.&lt;/strong&gt; El riel se renderizaba dentro de la misma página a la que enlazaba su botón, así que el click actualizaba el query string y la página se quedaba ahí. Peor, el enlace no cargaba id de match, así que una petición enviada nunca podía reportar de vuelta y la máquina de estados nunca podía avanzar más allá de &lt;code&gt;CLICKED&lt;/code&gt;. La tasa de aceptación, la única métrica sobre la que se juzga el rollout y la que está cableada a la alarma, se habría leído como &lt;strong&gt;cero para siempre&lt;/strong&gt;. El botón muerto se habría&lt;br&gt;
reportado en un día; la métrica muerta se habría creído por semanas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trampa 3: una auto-recomendación latente.&lt;/strong&gt; Cero filas en producción tenían &lt;code&gt;requester_id = expert_id&lt;/code&gt;, pero el clustering uno-a-muchos escogía su experto del pool completo sin excluir el cluster, y esas filas cargan al primer miembro del grupo como quien pide. Podía apuntarse a sí mismo. Simplemente nunca había&lt;br&gt;
disparado porque ningún cluster había alcanzado el umbral.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lecciones
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mide contra datos de producción antes de confiar en un diseño.&lt;/strong&gt; La primera dry run contra filas reales invalidó más del diseño que todas las pruebas juntas. Córrela antes de que se emita nada, no después.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Un recomendador para un recurso finito es un problema de asignación.&lt;/strong&gt; El top-N por usuario es la forma equivocada y su modo de falla es destruir el lado de la oferta. La restricción de capacidad va en la asignación, no en un post filtro.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chequea la tasa base de cualquier señal de traslape de conjuntos.&lt;/strong&gt; El lift, la frecuencia de documento y la rareza de habilidad son la misma corrección. Una señal compartida por la mitad de la población no carga información sobre un par, no importa qué tan cierta sea.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Antes de afinar un umbral, verifica el vocabulario.&lt;/strong&gt; Se gastaron dos rondas en cortes de frecuencia para un conjunto de tags que no contenía ninguna tecnología en absoluto. Pregúntate qué &lt;em&gt;son&lt;/em&gt; las etiquetas antes de preguntar qué tan comunes son.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Un intervalo de confianza es una decisión de producto, no un detalle de estadística.&lt;/strong&gt; El intervalo, el piso de muestra y la cota de efecto son lo que se para entre una correlación y una promesa que los datos no pueden cumplir. Muestra el rango, declara la muestra, y niégate a imprimir el número cuando las compuertas fallan.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Siembra el bootstrap.&lt;/strong&gt; Un número que cambia al recargar es indistinguible de un bug, y destruye la confianza del operador más rápido que estar mal una vez.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instrumenta la conversión antes de publicar la superficie.&lt;/strong&gt; Un botón muerto se reporta en un día. Una métrica muerta se cree por un mes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Publica el motor a oscuras.&lt;/strong&gt; Emisión apagada por default y una dry run como la acción de admin por default significaron que cuatro bugs significativos se encontraron con datos reales y cero notificaciones enviadas.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>algorithms</category>
      <category>datascience</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Building PickTool with Next.js and Laravel: Lessons from Creating a Software Discovery Platform</title>
      <dc:creator>Doaa F M Abed</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:01:27 +0000</pubDate>
      <link>https://dev.to/doaaabed94/building-picktool-with-nextjs-and-laravel-lessons-from-creating-a-software-discovery-platform-d72</link>
      <guid>https://dev.to/doaaabed94/building-picktool-with-nextjs-and-laravel-lessons-from-creating-a-software-discovery-platform-d72</guid>
      <description>&lt;p&gt;Finding software is easy. Finding the &lt;strong&gt;right&lt;/strong&gt; software is not.&lt;/p&gt;

&lt;p&gt;Search for almost any category—email marketing, CRM, productivity, design, or AI—and you will find hundreds of options. Every product presents itself as the best choice, while many comparison articles repeat the same features without explaining which users each tool actually suits.&lt;/p&gt;

&lt;p&gt;That problem inspired me to build &lt;a href="https://picktool.io/" rel="noopener noreferrer"&gt;PickTool&lt;/a&gt;, a platform for discovering and comparing AI and SaaS tools.&lt;/p&gt;

&lt;p&gt;PickTool is still evolving. I am currently improving its content quality, tool coverage, comparison experience, performance, and SEO structure.&lt;/p&gt;

&lt;p&gt;This is not a polished launch announcement. It is an honest look at the architecture behind the project and some of the lessons I have learned while building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is PickTool?
&lt;/h2&gt;

&lt;p&gt;The goal of PickTool is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Help people find the right software in minutes, not hours.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of creating a basic directory filled with product names and affiliate links, I want each important tool to include useful and structured information, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Core features&lt;/li&gt;
&lt;li&gt;Pricing model&lt;/li&gt;
&lt;li&gt;Best use cases&lt;/li&gt;
&lt;li&gt;Strengths and limitations&lt;/li&gt;
&lt;li&gt;Ratings and evaluation criteria&lt;/li&gt;
&lt;li&gt;Alternatives&lt;/li&gt;
&lt;li&gt;Direct comparisons&lt;/li&gt;
&lt;li&gt;Related guides and category pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The challenge is that this creates several interconnected types of content.&lt;/p&gt;

&lt;p&gt;A single product can appear on its own tool page, inside a category, in multiple comparisons, and in articles about the best software for a particular use case.&lt;/p&gt;

&lt;p&gt;Keeping all of this consistent requires more than publishing isolated blog posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Chose Next.js and Laravel
&lt;/h2&gt;

&lt;p&gt;PickTool uses a decoupled architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt; powers the public-facing website.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel&lt;/strong&gt; powers the backend, API, database logic, and administration system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL&lt;/strong&gt; stores tools, categories, ratings, pricing information, and editorial content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose this combination because I wanted the frontend and content-management logic to evolve independently.&lt;/p&gt;

&lt;p&gt;Laravel provides a structured backend for managing relationships between tools and content. Next.js gives me control over rendering, metadata, performance, and the public user experience.&lt;/p&gt;

&lt;p&gt;This separation also allows me to improve the frontend without rebuilding the entire administration workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modeling More Than a List of Tools
&lt;/h2&gt;

&lt;p&gt;One of the first lessons I learned was that a software directory should not be modeled as one large table of products.&lt;/p&gt;

&lt;p&gt;The data is relational.&lt;/p&gt;

&lt;p&gt;A tool belongs to a category, but it can also have several competitors, alternatives, comparison pages, pricing plans, use cases, and editorial guides.&lt;/p&gt;

&lt;p&gt;Conceptually, the content structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Category
├── Tools
├── Best-software guides
├── Comparison pages
└── Supporting articles

Tool
├── Features
├── Pricing
├── Pros and cons
├── Alternatives
├── Comparisons
└── Related guides
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure makes internal linking more intentional.&lt;/p&gt;

&lt;p&gt;For example, an email marketing tool can link to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Its main email marketing category&lt;/li&gt;
&lt;li&gt;Relevant alternatives&lt;/li&gt;
&lt;li&gt;Direct competitors&lt;/li&gt;
&lt;li&gt;A comparison article&lt;/li&gt;
&lt;li&gt;A best email marketing software guide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The technical data model and the SEO architecture are closely connected.&lt;/p&gt;

&lt;p&gt;If these relationships are not represented clearly in the database, keeping the public pages organized becomes much harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Scaling Content Too Early
&lt;/h2&gt;

&lt;p&gt;When building a software directory, it is tempting to believe that adding more products automatically makes the website more useful.&lt;/p&gt;

&lt;p&gt;I learned that quantity can create problems very quickly.&lt;/p&gt;

&lt;p&gt;A directory containing hundreds of incomplete or repetitive pages is not necessarily more valuable than one containing twenty carefully developed product profiles.&lt;/p&gt;

&lt;p&gt;Scaling too early can lead to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Thin pages&lt;/li&gt;
&lt;li&gt;Repetitive descriptions&lt;/li&gt;
&lt;li&gt;Outdated pricing&lt;/li&gt;
&lt;li&gt;Weak internal linking&lt;/li&gt;
&lt;li&gt;Inconsistent ratings&lt;/li&gt;
&lt;li&gt;Multiple pages targeting almost identical keywords&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am now taking a more focused approach.&lt;/p&gt;

&lt;p&gt;Instead of trying to cover every software category equally, I am strengthening one topic cluster at a time. Email marketing is one of the first categories receiving this treatment.&lt;/p&gt;

&lt;p&gt;That means improving the main category page, completing the most important tool profiles, and connecting them with comparisons, alternatives, and use-case guides.&lt;/p&gt;

&lt;p&gt;It is slower than bulk publishing, but it creates a stronger foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building SEO into the Application
&lt;/h2&gt;

&lt;p&gt;Because PickTool depends heavily on organic discovery, SEO cannot be added as a final plugin or checklist.&lt;/p&gt;

&lt;p&gt;It has to be part of the application architecture.&lt;/p&gt;

&lt;p&gt;Every indexable page needs its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page title&lt;/li&gt;
&lt;li&gt;Meta description&lt;/li&gt;
&lt;li&gt;Canonical URL&lt;/li&gt;
&lt;li&gt;Heading structure&lt;/li&gt;
&lt;li&gt;Structured data&lt;/li&gt;
&lt;li&gt;Internal links&lt;/li&gt;
&lt;li&gt;Open Graph information&lt;/li&gt;
&lt;li&gt;Indexing rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dynamic pages make this more complicated.&lt;/p&gt;

&lt;p&gt;A missing canonical rule or an incorrect URL pattern can affect hundreds of pages at once. The same applies to duplicated metadata, empty category pages, parameterized URLs, and pages generated from incomplete database records.&lt;/p&gt;

&lt;p&gt;I have learned to treat SEO templates like application code: they need clear rules, validation, testing, and regular audits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Is Also a Content Problem
&lt;/h2&gt;

&lt;p&gt;When developers discuss performance, we often focus on JavaScript bundles, caching, image sizes, and server response times.&lt;/p&gt;

&lt;p&gt;Those things matter, but content structure affects performance too.&lt;/p&gt;

&lt;p&gt;A tool page can become unnecessarily heavy when it tries to load:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The complete tool profile&lt;/li&gt;
&lt;li&gt;Multiple pricing plans&lt;/li&gt;
&lt;li&gt;Alternative tools&lt;/li&gt;
&lt;li&gt;Comparison tables&lt;/li&gt;
&lt;li&gt;Related articles&lt;/li&gt;
&lt;li&gt;Interactive interface elements&lt;/li&gt;
&lt;li&gt;Several large images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am working on keeping the initial page useful without loading every possible component immediately.&lt;/p&gt;

&lt;p&gt;Some of the areas I continue to review include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Image optimization&lt;/li&gt;
&lt;li&gt;Server-side data fetching&lt;/li&gt;
&lt;li&gt;Caching&lt;/li&gt;
&lt;li&gt;Component size&lt;/li&gt;
&lt;li&gt;Unnecessary client-side JavaScript&lt;/li&gt;
&lt;li&gt;API response size&lt;/li&gt;
&lt;li&gt;Core Web Vitals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a content-heavy platform, performance is not something that gets permanently “finished.” It requires continuous measurement as the website grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the Backend and Frontend Consistent
&lt;/h2&gt;

&lt;p&gt;A decoupled Laravel and Next.js architecture provides flexibility, but it also introduces a consistency challenge.&lt;/p&gt;

&lt;p&gt;The frontend depends on the backend to return complete and predictable data. If fields are missing or relationships are inconsistent, the public pages can show empty sections or incomplete information.&lt;/p&gt;

&lt;p&gt;This pushed me to think more carefully about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API response structures&lt;/li&gt;
&lt;li&gt;Required and optional fields&lt;/li&gt;
&lt;li&gt;Validation in the admin system&lt;/li&gt;
&lt;li&gt;Published and unpublished states&lt;/li&gt;
&lt;li&gt;Default values&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;Content freshness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A page should not be published simply because a database record exists.&lt;/p&gt;

&lt;p&gt;It should be published when it contains enough information to be genuinely useful.&lt;/p&gt;

&lt;p&gt;That distinction is becoming an important part of the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transparency Matters for Comparison Platforms
&lt;/h2&gt;

&lt;p&gt;A comparison platform needs more than attractive cards and numerical ratings.&lt;/p&gt;

&lt;p&gt;Visitors should be able to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How tools are evaluated&lt;/li&gt;
&lt;li&gt;Which factors affect their scores&lt;/li&gt;
&lt;li&gt;When information was last updated&lt;/li&gt;
&lt;li&gt;Whether affiliate relationships exist&lt;/li&gt;
&lt;li&gt;Where pricing information comes from&lt;/li&gt;
&lt;li&gt;Who each recommendation is intended for&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is both an editorial and technical challenge.&lt;/p&gt;

&lt;p&gt;The backend needs to support structured evaluation data, while the frontend needs to present that information without overwhelming the visitor.&lt;/p&gt;

&lt;p&gt;I am still improving this part of PickTool. My goal is to make ratings and recommendations explainable instead of presenting numerical scores without context.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;If I were starting the project again, I would make several decisions earlier.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Start With One Narrow Category
&lt;/h3&gt;

&lt;p&gt;I would build a complete experience around one software category before expanding into many unrelated categories.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Define Minimum Publishing Requirements
&lt;/h3&gt;

&lt;p&gt;I would establish a minimum content standard before allowing a tool page to become publicly indexable.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Design Internal Linking With the Data Model
&lt;/h3&gt;

&lt;p&gt;Internal links should come from meaningful relationships between tools and topics, not from randomly selected related content.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Separate Discovery From Editorial Content
&lt;/h3&gt;

&lt;p&gt;A directory page, a review, a direct comparison, and a best-software guide serve different user and search intentions.&lt;/p&gt;

&lt;p&gt;They should not be treated as interchangeable templates.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Build Auditing Into the Workflow
&lt;/h3&gt;

&lt;p&gt;Metadata, canonical URLs, broken links, incomplete fields, and outdated pricing should be checked regularly instead of waiting for problems to appear in search reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Comes Next?
&lt;/h2&gt;

&lt;p&gt;PickTool is not finished, and I do not think a platform like this ever reaches a permanent finished state.&lt;/p&gt;

&lt;p&gt;My current priorities are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strengthening the email marketing software cluster&lt;/li&gt;
&lt;li&gt;Improving the quality of individual tool pages&lt;/li&gt;
&lt;li&gt;Making comparisons more useful&lt;/li&gt;
&lt;li&gt;Refining the rating methodology&lt;/li&gt;
&lt;li&gt;Improving technical SEO&lt;/li&gt;
&lt;li&gt;Monitoring performance as content grows&lt;/li&gt;
&lt;li&gt;Creating a clearer path from discovery to decision&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building this project has reminded me that creating a useful software platform is not only about writing code or publishing content.&lt;/p&gt;

&lt;p&gt;It requires connecting product design, data modeling, performance, editorial standards, and search architecture into one consistent experience.&lt;/p&gt;

&lt;p&gt;You can explore the current version at &lt;a href="https://picktool.io/" rel="noopener noreferrer"&gt;PickTool.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I am building it in public and improving it step by step. Feedback from developers, SaaS users, and product builders is always welcome.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What challenges have you faced while building a content-heavy platform with a separate frontend and backend?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>laravel</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Planning Over Execution: Lessons from 157 Agent Runs and the Rise of Orca-Style Agent Fleets</title>
      <dc:creator>Tamiz Uddin</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:00:51 +0000</pubDate>
      <link>https://dev.to/tamizuddin/planning-over-execution-lessons-from-157-agent-runs-and-the-rise-of-orca-style-agent-fleets-3cb8</link>
      <guid>https://dev.to/tamizuddin/planning-over-execution-lessons-from-157-agent-runs-and-the-rise-of-orca-style-agent-fleets-3cb8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://tamiz.pro/insights/planning-over-execution-157-agent-runs-orca-fleets" rel="noopener noreferrer"&gt;tamiz.pro&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The field of AI agents has moved rapidly from single-model executors to complex multi-agent orchestration. But after running 157 agent deployments across diverse task domains, one pattern emerged with striking consistency: &lt;strong&gt;planning quality predicts success far better than execution speed or model size.&lt;/strong&gt; This isn't just theoretical—it's a practical lesson that's reshaping how engineers architect agent fleets, giving rise to what we're now calling &lt;strong&gt;Orca-style agents&lt;/strong&gt;: hierarchical, planning-first systems that separate the expensive business of thinking from the cheaper business of doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Experiment: 157 Agent Runs
&lt;/h2&gt;

&lt;p&gt;Over six months, our team deployed and monitored 157 distinct agent runs across four primary use cases: code generation pipelines, automated testing workflows, infrastructure-as-code provisioning, and data transformation tasks. Each run varied along three dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Architecture&lt;/strong&gt;: Single-agent vs. flat multi-agent vs. hierarchical (Orca-style)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Planning depth&lt;/strong&gt;: No planning, brief intent statement, or full recursive planning loop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution model&lt;/strong&gt;: Direct LLM call per action vs. tool-augmented execution with validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The results were unambiguous. Systems that invested 3-5x more tokens in planning achieved 4.2x higher task completion rates and 3.8x fewer rollback cycles compared to agents optimized purely for fast execution. The correlation between planning sophistication and success held across every domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Planning Beats Raw Execution
&lt;/h2&gt;

&lt;p&gt;The intuition behind this finding rests on an economic principle of LLM usage: &lt;strong&gt;planning is cheap relative to costly mistakes&lt;/strong&gt;. A well-structured plan reduces the probability of executing the wrong sequence of tools, making incorrect API calls, or generating code that fails integration testing.&lt;/p&gt;

&lt;p&gt;Consider the token economics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Tokens (typical)&lt;/th&gt;
&lt;th&gt;Cost impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Planning (intent + decomposition)&lt;/td&gt;
&lt;td&gt;800–2,500&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution per subtask&lt;/td&gt;
&lt;td&gt;300–1,200&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correction after failure&lt;/td&gt;
&lt;td&gt;1,500–4,000&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Agents that plan thoroughly front-load their costs. Those that rush to execute often pay exponentially more in corrections, retries, and human intervention.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Orca Architecture Pattern
&lt;/h2&gt;

&lt;p&gt;The name "Orca" comes from the hierarchical social structure of killer whales: a single matriarch orchestrates, while specialized pod members execute discrete tasks. In agent terms, this translates to:&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Components
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Strategic Planner&lt;/strong&gt; (the matriarch): Holds global context, decomposes goals, assigns subtasks, and validates outcomes. Runs on a stronger model with longer context windows.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Specialist Executors&lt;/strong&gt; (the pod): Each handles a narrow domain—code generation, test writing, documentation, validation. Run on smaller, cheaper models optimized for throughput.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Shared Memory Layer&lt;/strong&gt;: A structured knowledge graph or vector store that maintains state across the fleet, preventing redundant work and enabling cross-agent learning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Orchestration Loop&lt;/strong&gt;: A lightweight controller that routes tasks, aggregates results, and triggers replanning when validation fails.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why This Separation Matters
&lt;/h3&gt;

&lt;p&gt;The critical insight is that &lt;strong&gt;not all thinking is equal&lt;/strong&gt;. Strategic decisions—understanding requirements, identifying edge cases, sequencing dependencies—benefit from deep context and reasoning. Tactical decisions—formatting output, calling a specific API, generating a template—are better handled by focused, optimized models.&lt;/p&gt;

&lt;p&gt;Separating these concerns allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run planners on premium models without paying premium prices for every action&lt;/li&gt;
&lt;li&gt;Scale executor capacity independently of planning capacity&lt;/li&gt;
&lt;li&gt;Implement targeted retry logic without restarting entire workflows&lt;/li&gt;
&lt;li&gt;Observe and debug planning failures separately from execution failures&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation Patterns
&lt;/h2&gt;

&lt;p&gt;From the 157 runs, several implementation patterns emerged as particularly effective:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: Recursive Decomposition with Validation Gates
&lt;/h3&gt;

&lt;p&gt;The planner decomposes a goal into subtasks, each with explicit success criteria. Executors complete subtasks and return structured evidence of completion. The planner validates before proceeding to the next level.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrcaPlanner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decompose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Subtask&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Recursive planning with validation gates.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;validated_subtasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;subtask&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subtasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;subtask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requires_decisions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="c1"&gt;# Recursive planning for complex subtasks
&lt;/span&gt;                &lt;span class="n"&gt;sub_plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;validated_subtasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sub_plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;validated_subtasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;validated_subtasks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 2: Specialist Routing with Skill Cards
&lt;/h3&gt;

&lt;p&gt;Each executor carries a "skill card"—a concise description of its capabilities, constraints, and preferred input/output formats. The planner matches subtasks to specialists based on these cards rather than attempting blind routing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 3: Stateful Context Propagation
&lt;/h3&gt;

&lt;p&gt;Instead of passing raw conversation history, agents exchange structured context objects: constraints discovered, assumptions made, partial results, and confidence scores. This enables better downstream planning and reduces context window waste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls from the 157 Runs
&lt;/h2&gt;

&lt;p&gt;Not every design decision landed well. Here are the patterns that correlated with failure:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 1: Over-Planning
&lt;/h3&gt;

&lt;p&gt;Some teams spent so much time planning that the plan became stale before execution began. The sweet spot was ~20% of total token budget for planning, with dynamic replanning triggered only by validation failures—not on a timer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 2: Specialist Fragmentation
&lt;/h3&gt;

&lt;p&gt;Creating too many specialists (15+) introduced routing overhead and context fragmentation. The optimal range was 4–8 specialists, each covering a distinct capability domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 3: Silent Replanning
&lt;/h3&gt;

&lt;p&gt;When execution failed, some systems silently retried with minor variations. Successful systems explicitly logged failures, triggered replanning at the appropriate abstraction layer, and maintained an audit trail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 4: Context Window Hoarding
&lt;/h3&gt;

&lt;p&gt;Planning agents that retained full conversation history from execution agents burned through context windows unnecessarily. Successful implementations used summarized state objects instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Orca-Style Fleets
&lt;/h2&gt;

&lt;p&gt;These systems aren't a universal upgrade. Based on the data:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single-shot code generation&lt;/td&gt;
&lt;td&gt;Single agent sufficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-step workflows with validation&lt;/td&gt;
&lt;td&gt;Orca-style strongly recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-stakes operations (infra, payments)&lt;/td&gt;
&lt;td&gt;Orca-style with human-in-the-loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch processing (100+ items)&lt;/td&gt;
&lt;td&gt;Hybrid: planner + parallel executors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interactive assistance&lt;/td&gt;
&lt;td&gt;Lightweight planning, direct execution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The break-even point appears around 3–5 sequential steps with dependency checking. Below that, the planning overhead outweighs the benefits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Economics of Planning-First Design
&lt;/h2&gt;

&lt;p&gt;For engineers evaluating whether to adopt Orca-style architectures, the cost model matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traditional approach&lt;/strong&gt;: One large model handles everything. Cheap for simple tasks, expensive for complex ones due to rework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orca approach&lt;/strong&gt;: Premium model for planning, lighter models for execution. Higher upfront cost, dramatically lower correction costs.&lt;/p&gt;

&lt;p&gt;In our measurements, the Orca architecture showed a &lt;strong&gt;37% reduction in total token cost&lt;/strong&gt; for tasks exceeding 10 steps, despite using more expensive models for planning. The savings came from reduced retry loops and fewer human escalations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Ahead
&lt;/h2&gt;

&lt;p&gt;The 157-agent study reveals a fundamental shift in how we should think about agentic systems. The question is no longer "how fast can the agent execute?" but "how well can the agent plan?" This reframing is driving a new generation of tools focused on planning quality: structured decomposers, validation-aware schedulers, and context-efficient state management.&lt;/p&gt;

&lt;p&gt;As the field matures, we'll likely see planning becomes a first-class concern in agent frameworks, with libraries and patterns emerging specifically for the strategic layer. The Orca architecture isn't just a pattern—it's an acknowledgment that &lt;strong&gt;thinking carefully is the most important part of the job&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I implement Orca-style planning without a full multi-agent framework?&lt;/strong&gt;&lt;br&gt;
A: Yes. Start with a single planner function that decomposes goals and validates each step before execution. You don't need separate agents initially—a well-structured planning loop within a single process captures 80% of the benefit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I choose which model to use for planning vs. execution?&lt;/strong&gt;&lt;br&gt;
A: Planning benefits from strong reasoning and long context (e.g., GPT-4-class models). Execution can use smaller, faster models optimized for specific tasks. The key is matching model capability to cognitive demand, not cost alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's the minimum viable Orca architecture?&lt;/strong&gt;&lt;br&gt;
A: Three components: (1) a planner that decomposes goals into sequenced subtasks with success criteria, (2) an executor that runs subtasks and returns structured results, and (3) a validator that checks completion evidence before allowing progression. This fits in under 200 lines of code.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machine</category>
      <category>learning</category>
      <category>planning</category>
    </item>
    <item>
      <title>Four Alarm Slots, Three Failure Modes: Building a Nightly Drain That Survives Sleep, Races, and Timeouts</title>
      <dc:creator>Lily</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:00:06 +0000</pubDate>
      <link>https://dev.to/bokuwalily/four-alarm-slots-three-failure-modes-building-a-nightly-drain-that-survives-sleep-races-and-5663</link>
      <guid>https://dev.to/bokuwalily/four-alarm-slots-three-failure-modes-building-a-nightly-drain-that-survives-sleep-races-and-5663</guid>
      <description>&lt;p&gt;Every night my Mac quietly rewrites my long-term memory. Not metaphorically — a shell script drains that day's Claude Code conversation logs into an Obsidian vault, commits them to a private repo, and leaves a briefing on my desktop. It took three real outages to make it reliable. This is the script, the three failures, and the design that came out of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Setup Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Claude Code's "memory" disappears by default
&lt;/h3&gt;

&lt;p&gt;Claude Code sessions are independent of one another. The root cause of a bug you found during a long working session today, the reason you settled on a particular architecture after trial and error, the accumulated knowledge that "this direction already failed once" — none of it is available in the next conversation once you close the session.&lt;/p&gt;

&lt;p&gt;Even on a paid plan, even with the most capable model available, if context isn't carried over you have to explain everything from scratch every time. Many people have had the experience of thinking "I already looked this up before" or "I should have failed at this once already, and yet here I am heading down the same road again."&lt;/p&gt;

&lt;p&gt;In a phase where you're shipping personal projects in volume, this problem is fatal. Once three or four projects are running in parallel, tracking "where each project currently stands" by hand hits a wall fast. And Claude, unable to reference previous conversations, repeats the same deliberations.&lt;/p&gt;

&lt;h3&gt;
  
  
  The solution is to build an environment, not a task
&lt;/h3&gt;

&lt;p&gt;My first attempt at this problem was "I'll write up a summary by hand every day." It didn't last. When work has momentum you don't feel like writing a summary, and when you're tired you can write even less. A system that depends on human willpower doesn't function during a high-volume solo-dev phase.&lt;/p&gt;

&lt;p&gt;The answer was to build an environment that automatically drains Claude's conversation logs into Obsidian every night. Once the environment is in place, willpower and motivation are irrelevant. The Mac just does it.&lt;/p&gt;

&lt;p&gt;The reason I chose Obsidian is simple. The files are local Markdown, so Claude Code can read and write them directly. They can be version-controlled with Git. The &lt;code&gt;[[link]]&lt;/code&gt; syntax lets you connect pieces of knowledge to each other. Logs flow in every morning and cross-project links grow naturally — from the moment this started functioning as an "external brain," the quality of my work changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a triple-layered structure became necessary
&lt;/h3&gt;

&lt;p&gt;Simply "running a script at 4:55 every day" produced three distinct kinds of failure once I actually ran it. Each one only became apparent after it caused real damage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sleep freeze&lt;/strong&gt;: If you close the lid on the Mac and go to bed, &lt;code&gt;caffeinate -s&lt;/code&gt; (which only takes effect on AC power) can't prevent sleep. The script stops partway through, and that day's processing hangs in limbo until the next slot fires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Double-execution race&lt;/strong&gt;: An actual incident on 2026-06-10. A scheduled launchd firing overlapped with a manual run. Both tried to operate on the same vault with Git, and the result was a conflict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeout&lt;/strong&gt;: On 2026-06-13, every slot failed for the entire day. Digesting 28 hours' worth of logs via &lt;code&gt;claude -p&lt;/code&gt; didn't fit within the 40-minute timeout window and all of it got culled. In the logs, the pattern &lt;code&gt;started 04:55:00 → step2 timeout at 05:40:01&lt;/code&gt; lines up across all four slots, completely uniform.&lt;/p&gt;

&lt;p&gt;These three real failures are what forced the triple-layered structure of "multi-slot re-firing," "caffeinate sleep prevention," and "idempotent retry via step markers." It didn't come out of a design document; it accumulated from things that actually broke.&lt;/p&gt;

&lt;h3&gt;
  
  
  The feeling of an environment that grows
&lt;/h3&gt;

&lt;p&gt;Once the system started running stably, logs pile up in Obsidian every morning. Claude reads those logs, updates per-project articles, and this week's talking points accumulate in &lt;code&gt;hot.md&lt;/code&gt; (a summary of recent context).&lt;/p&gt;

&lt;p&gt;When Claude reads &lt;code&gt;hot.md&lt;/code&gt; and &lt;code&gt;wiki/&lt;/code&gt; in the next conversation, it can start working already knowing "last week's decisions," "the approach that failed once," and "the current state of the three projects running in parallel." That's an external long-term memory that doesn't forget when the conversation ends — a prerequisite for getting Claude Code to perform at its actual potential.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Overall Flow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Four-layer architecture
&lt;/h3&gt;

&lt;p&gt;The data the script handles flows through the following four layers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;【層1】Claude Code セッションログ（セッション終了時にStop hookが書き出し）
           ↓ extract_conversations.py（step1: 最新化）
【層2】~/Documents/my-knowledge-base/raw/conversations/
           ↓ claude -p（step2a: Claude由来ログを消化, timeout 1500s）
       ~/Documents/my-knowledge-base/raw/codex-conversations/
           ↓ claude -p（step2b: Codex由来ログを消化, timeout 1500s）
【層3】Obsidian Vault（~/Documents/claude-obsidian/wiki/）
       ├── hot.md（直近サマリ）
       ├── index.md（全体目次）
       ├── projects/ / learning/ / career/ ... （ドメイン別記事）
       └── today-brief.md（step2.5: 今日の行動提案）
           ↓ git add -A &amp;amp;&amp;amp; git commit &amp;amp;&amp;amp; git push
【層4】private repo（安全網: 荒れてもrevert可能）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Splitting layer 2 into two lines, "Claude-derived" and "Codex-derived," is a change made on or after 2026-06-11. Originally one line processed everything, but on high-activity days it stopped fitting into the 40-minute timeout window. After the split, each has an independent 1500-second (25-minute) timeout, and if one fails the next slot can retry only what's left.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing multi-slot firing
&lt;/h3&gt;

&lt;p&gt;Four slots defined in a launchd plist (&lt;code&gt;~/Library/LaunchAgents/com.shun.vault-auto-ingest.plist&lt;/code&gt;) spend the day repeatedly "retrying until success."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StartCalendarInterval&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;4&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;55&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;8&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;20&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;45&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&amp;lt;key&amp;gt;&lt;/span&gt;Hour&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;12&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&amp;lt;key&amp;gt;&lt;/span&gt;Minute&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;15&lt;span class="nt"&gt;&amp;lt;/integer&amp;gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RunAtLoad&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;. There's no auto-start at login. The four slots are the only firing sources.&lt;/p&gt;

&lt;p&gt;At the top of the script it checks for the day's success marker and exits immediately if it's already finished.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TODAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;DONE_MARKER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs/.vault-ingest-done-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TODAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 0. 本日分が既に成功していれば即終了&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DONE_MARKER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, if 4:55 succeeds then 8:20, 10:45, and 12:15 become harmless no-ops that just "check the file and exit." Only if 4:55 fails (sleep freeze, no network, a transient launchd fault, etc.) does 8:20 actually take over the processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sleep prevention with caffeinate
&lt;/h3&gt;

&lt;p&gt;When running this on a MacBook, closing the lid is something &lt;code&gt;caffeinate -s&lt;/code&gt; (only effective on AC power) can't fully prevent. So the script re-executes itself under both &lt;code&gt;-i&lt;/code&gt; (prevent system sleep) and &lt;code&gt;-s&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CAFFEINATED&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; /usr/bin/caffeinate &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nb"&gt;env &lt;/span&gt;&lt;span class="nv"&gt;CAFFEINATED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 /bin/bash &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the &lt;code&gt;CAFFEINATED&lt;/code&gt; environment variable isn't set, it re-executes itself under &lt;code&gt;caffeinate&lt;/code&gt; and puts everything after that under caffeinate's umbrella. Since &lt;code&gt;CAFFEINATED=1&lt;/code&gt; is set after the single re-execution, there's no infinite loop.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-s&lt;/code&gt; has no effect on battery power. If it freezes from lid-close sleep, &lt;code&gt;timeout&lt;/code&gt; culls it and the next slot starts over. If step2 got partway through, the markers let it skip what's done and run only the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preventing double execution with a lockdir
&lt;/h3&gt;

&lt;p&gt;This is a locking mechanism that uses the atomicity of &lt;code&gt;mkdir&lt;/code&gt; (if two processes call it simultaneously, only one succeeds). It's a widely used technique for file locking in Bash.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;LOCKDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/locks/vault-auto-ingest.lock"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; /bin/mkdir &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;oldpid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKDIR&lt;/span&gt;&lt;span class="s2"&gt;/pid"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;oldpid&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$oldpid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%F %T'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;] 別インスタンス実行中(pid=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;oldpid&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) — skip"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  /bin/mkdir &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$$&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKDIR&lt;/span&gt;&lt;span class="s2"&gt;/pid"&lt;/span&gt;
&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'rm -rf "$LOCKDIR"'&lt;/span&gt; EXIT INT TERM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even when an existing lockdir is present, it checks the PID, and if the process is alive it exits as "another instance is running." If the process is dead (a lock left behind by an abnormal termination), it reclaims it as stale, acquires a new lock, and continues. &lt;code&gt;trap&lt;/code&gt; guarantees the lockdir is deleted when the script exits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idempotent retry via step markers
&lt;/h3&gt;

&lt;p&gt;This is the part that took the most work. step2 (the ingest into the vault) is split into two lines, Claude and Codex, each with its own independent marker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;STEP2A_MARKER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs/.vault-ingest-step2a-claude-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TODAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;STEP2B_MARKER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs/.vault-ingest-step2b-codex-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TODAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ingest_src&lt;/code&gt; function looks at these markers to decide "skip if already complete, run if not."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ingest_src&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;marker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$4&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$5&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$marker&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[...] step2(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;) は本日実施済み — skip"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; run_to &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$to&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"...（Vaultのルールに従って wiki/ を更新するプロンプト）..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--dangerously-skip-permissions&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$marker&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[...] WARN: step2(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;) 失敗/timeout（次スロットで再試行）"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

ingest_src &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STEP2A_MARKER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KB&lt;/span&gt;&lt;span class="s2"&gt;/raw/conversations/"&lt;/span&gt; &lt;span class="s2"&gt;"claude"&lt;/span&gt; 1500 &lt;span class="s2"&gt;""&lt;/span&gt;
ingest_src &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STEP2B_MARKER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KB&lt;/span&gt;&lt;span class="s2"&gt;/raw/codex-conversations/"&lt;/span&gt; &lt;span class="s2"&gt;"codex"&lt;/span&gt; 1500 &lt;span class="s2"&gt;"Codex由来でも既存記事に統合し重複は追記でまとめろ。"&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STEP2A_MARKER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STEP2B_MARKER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STEP2_MARKER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The behavior pattern is as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4:55 発火
├── DONE_MARKER なし → 処理続行
├── step2a 実行（claude由来, 1500s上限） → 成功 → STEP2A_MARKER 作成
├── step2b 実行（codex由来, 1500s上限） → timeout! → マーカーなし
├── brief生成 → 失敗（step2b未完でログが薄い） → DONE_MARKER 作らない
└── notify_fail で Desktop に FAILED ファイル + 通知

8:20 発火
├── DONE_MARKER なし → 処理続行
├── step2a → STEP2A_MARKER あり → skip（再実行しない）
├── step2b → マーカーなし → 実行 → 成功 → STEP2B_MARKER 作成
├── brief生成 → 成功
├── DONE_MARKER 作成 ✓
└── FAILED ファイル削除

10:45 / 12:15 発火
└── DONE_MARKER あり → exit 0（空振り）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If step2a succeeded, 8:20 runs only step2b. If both step2a and step2b are finished, 8:20 runs only the brief. Because which slot is responsible for what is determined dynamically, the guarantee that "even if 4:55 fails, it will definitely finish within the day" is preserved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filling in launchd PATH and the node binary
&lt;/h3&gt;

&lt;p&gt;launchd's execution environment has only a minimal PATH, roughly &lt;code&gt;/usr/bin:/bin:/usr/sbin:/sbin&lt;/code&gt;. To avoid the trap where the post-&lt;code&gt;git commit&lt;/code&gt; hook (written in node) doesn't run and the &lt;code&gt;commit&lt;/code&gt; itself fails, the top of the script auto-discovers the newest nvm node and adds it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NODE_BIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/.nvm/versions/node/&lt;span class="k"&gt;*&lt;/span&gt;/bin 2&amp;gt;/dev/null | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-V&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NODE_BIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_BIN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Taking the tail of &lt;code&gt;sort -V&lt;/code&gt; (version-order sort) means it always uses the newest even when nvm has multiple versions installed. Since no version is hardcoded, adding versions with &lt;code&gt;nvm install&lt;/code&gt; requires no changes to the script.&lt;/p&gt;

&lt;p&gt;Also, due to macOS TCC (privacy protection), launchd can't write under &lt;code&gt;~/Documents/&lt;/code&gt; unless &lt;code&gt;/bin/bash&lt;/code&gt; has Full Disk Access. The script detects this early to prevent silent failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git rev-parse &lt;span class="nt"&gt;--git-dir&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[...] ❌ FDA未付与: launchdから '&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;' にアクセス不可(TCC保護)。"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  notify_fail &lt;span class="s2"&gt;"FDA未付与: vault にアクセス不可（設定→フルディスクアクセス→/bin/bash）"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returning &lt;code&gt;exit 0&lt;/code&gt; would look like "it succeeded," DONE_MARKER would be created, and the next slot wouldn't retry. TCC failure is signaled explicitly with &lt;code&gt;exit 1&lt;/code&gt; and handed off to the next slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The timeout helper and designing for a guaranteed kill
&lt;/h3&gt;

&lt;p&gt;One of the script's core decisions is the &lt;code&gt;run_to&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TIMEOUT_BIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/homebrew/bin/timeout"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TIMEOUT_BIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;TIMEOUT_BIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
run_to&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TIMEOUT_BIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TIMEOUT_BIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--kill-after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It calls GNU coreutils' &lt;code&gt;timeout&lt;/code&gt; by explicit path. macOS's standard &lt;code&gt;/usr/bin&lt;/code&gt; doesn't have the GNU version of &lt;code&gt;timeout&lt;/code&gt;. In environments where &lt;code&gt;/opt/homebrew/bin/timeout&lt;/code&gt; doesn't exist, &lt;code&gt;TIMEOUT_BIN&lt;/code&gt; is left empty and calls pass through (hang resistance drops, but it keeps working).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--kill-after=30&lt;/code&gt; matters more than it looks. If the process is still alive 30 seconds after &lt;code&gt;SIGTERM&lt;/code&gt; is sent, it's force-killed with &lt;code&gt;SIGKILL&lt;/code&gt;. &lt;code&gt;claude -p&lt;/code&gt; sometimes ignores &lt;code&gt;SIGTERM&lt;/code&gt; while doing heavy work, so without &lt;code&gt;--kill-after&lt;/code&gt; you can hit the worst case: "the process survives past the timeout while still holding the lockdir."&lt;/p&gt;

&lt;p&gt;step2a (Claude-derived logs) and step2b (Codex-derived logs) each get a 1500-second (25-minute) limit. As mentioned earlier, it was originally a single 2400-second (40-minute) process, but as activity increased it stopped fitting. Splitting into two lines at 1500 seconds each means that if one dies, the next slot can re-run only the remainder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Waiting for the network after wake, and the preflight detection chain
&lt;/h3&gt;

&lt;p&gt;Even when the Mac wakes at 4:55, it can take tens of seconds for the Wi-Fi connection to stabilize. Both &lt;code&gt;claude&lt;/code&gt; and &lt;code&gt;git push&lt;/code&gt; need the network, so running before the connection is up is an immediate error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;net_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;_ &lt;span class="k"&gt;in &lt;/span&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if&lt;/span&gt; /usr/bin/nc &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; 3 1.1.1.1 443 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;net_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;5
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$net_ok&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[...] WARN: 網未接続のまま続行（失敗時は次スロットが再試行）"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;18 attempts × 5 seconds = up to 90 seconds of waiting. &lt;code&gt;nc -z -G 3&lt;/code&gt; is a TCP connection test that times out in 3 seconds, lighter than &lt;code&gt;curl&lt;/code&gt;. If it still isn't connected after 90 seconds, it "warns and continues." Using &lt;code&gt;exit 1&lt;/code&gt; here would also stop step1 (local processing) in an offline environment. Since the design already assumes the next slot auto-retries on failure, "record it without stopping" is enough here.&lt;/p&gt;

&lt;p&gt;Next, preflight detection chains through three stages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① Checking for the &lt;code&gt;claude&lt;/code&gt; binary&lt;/strong&gt;: This catches the case where an update removes the symlink target. Running &lt;code&gt;claude update&lt;/code&gt; can temporarily leave the binary absent, and this is a cause of silent failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② macOS TCC preflight&lt;/strong&gt;: If the vault is under &lt;code&gt;~/Documents/&lt;/code&gt;, a process running from launchd can't write to it without Full Disk Access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git rev-parse &lt;span class="nt"&gt;--git-dir&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[...] ❌ FDA未付与: launchdから '&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;' にアクセス不可(TCC保護)。"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  notify_fail &lt;span class="s2"&gt;"FDA未付与: vault にアクセス不可（設定→フルディスクアクセス→/bin/bash）"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returning &lt;code&gt;exit 0&lt;/code&gt; here would create DONE_MARKER, and the next slot would misread it as "succeeded" and not retry. TCC failure must always exit with &lt;code&gt;exit 1&lt;/code&gt; and hand off to the next slot. This distinction is also at the root of one of the failure stories below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Log bloat guard and brief freshness checks
&lt;/h3&gt;

&lt;p&gt;Writing all of &lt;code&gt;claude -p&lt;/code&gt;'s output to the log gets you to tens of MB within a few weeks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;%z &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 5242880 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.old"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it exceeds 5 MB (5,242,880 bytes), it renames the file to &lt;code&gt;.old&lt;/code&gt; and starts a new log. &lt;code&gt;stat -f%z&lt;/code&gt; is the macOS file-size command. Since the options differ from GNU &lt;code&gt;stat&lt;/code&gt;, &lt;code&gt;|| echo 0&lt;/code&gt; provides a fallback.&lt;/p&gt;

&lt;p&gt;The brief freshness check is another important part.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;START_STAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; /tmp/vault-ingest-start.XXXXXX&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This temp file is created at the top of the script, and after step2.5 (brief generation) it checks whether the brief is newer than it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BRIEF_SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BRIEF_SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-nt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$START_STAMP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c"&gt;# アーカイブ処理&lt;/span&gt;
  &lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DONE_MARKER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;notify_fail &lt;span class="s2"&gt;"ブリーフ生成が未完"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comparing with &lt;code&gt;-nt&lt;/code&gt; (newer than) prevents the accident of "mistakenly archiving yesterday's leftover &lt;code&gt;today-brief.md&lt;/code&gt; as today's output." If the brief isn't newer than the start of this run, it isn't treated as a success — DONE_MARKER isn't created and it hands off to the next slot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Overwriting LLM output with something deterministic
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;claude -p&lt;/code&gt; updates &lt;code&gt;wiki/index.md&lt;/code&gt;, it sometimes writes an incorrect value for "total page count." The model infers it from context, so it doesn't match the actual file count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;real&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;/wiki"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.md'&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s1"&gt;'*/.*'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"s/総ページ数：[0-9]+/総ページ数：&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;real&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INDEX_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It recounts the real file count with &lt;code&gt;find&lt;/code&gt; and force-overwrites with &lt;code&gt;sed&lt;/code&gt;. Because this runs immediately before &lt;code&gt;git commit&lt;/code&gt;, what gets committed has the LLM-generated number already replaced by a deterministic value. The design principle is: "numbers output by an LLM get overwritten by a deterministic check downstream."&lt;/p&gt;

&lt;h3&gt;
  
  
  The calendar last-known-good pattern
&lt;/h3&gt;

&lt;p&gt;When writing "today's schedule" into the brief, it retains past information on days when calendar data can't be fetched.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CAL_SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CAL_TMP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CAL_SNAPSHOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CAL_SNAPSHOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CAL_LASTGOOD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# 成功したら last-good を更新&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CAL_LASTGOOD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c"&gt;# 取得失敗: 前回 good を温存し stale 印を付けて出力&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; +4 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CAL_LASTGOOD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CAL_SNAPSHOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It tries the Google Calendar API (ADC auth) and icalBuddy (local Apple Calendar) in order, and if both fail it uses &lt;code&gt;_calendar-snapshot.md.lastgood&lt;/code&gt; from the last successful fetch. The principle is "never clobber the source of truth with empty." Staleness is marked explicitly with a leading &lt;code&gt;⚠️&lt;/code&gt;, and the brief-generation prompt is told to "note that the values are as of the fetch date if it says stale."&lt;/p&gt;




&lt;h2&gt;
  
  
  Where I Got Stuck
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Failure ①: The day every slot died at the same spot, to the second (2026-06-13)
&lt;/h3&gt;

&lt;p&gt;I noticed because &lt;code&gt;FAILED-20260613.md&lt;/code&gt; was sitting in &lt;code&gt;~/Desktop/Daily Brief/&lt;/code&gt;. Opening the log, all four slots showed exactly the same pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;04:55:00 ===== auto-ingest 開始 =====
05:40:01 WARN: step2(claude) 失敗/timeout（次スロットで再試行）
08:20:00 ===== auto-ingest 開始 =====
09:05:01 WARN: step2(claude) 失敗/timeout（次スロットで再試行）
10:45:00 ===== auto-ingest 開始 =====
11:30:01 WARN: step2(claude) 失敗/timeout（次スロットで再試行）
12:15:00 ===== auto-ingest 開始 =====
13:00:01 WARN: step2(claude) 失敗/timeout（次スロットで再試行）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every slot timed out at 45 minutes 01 second. The step2 timeout at the time was 2400 seconds (40 minutes). &lt;code&gt;claude -p&lt;/code&gt; kept processing for 40 minutes straight and got timed out, the next slot retried with the same result, and that loop repeated four times.&lt;/p&gt;

&lt;p&gt;What I suspected first was the error message after &lt;code&gt;git commit&lt;/code&gt;. The strings "commit-msg hook" and "node not found" were visible in the log, so I thought the node binary was the cause. But running &lt;code&gt;claude -p "say OK"&lt;/code&gt; by hand returned exit 0 immediately. Trying commit by hand worked fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real cause was a transient failure on the &lt;code&gt;claude -p&lt;/code&gt; side.&lt;/strong&gt; 06-12 worked normally; only on 06-13 did all requests hang (probably rate limiting or a temporary service-side fault), and it was resolved the next day. The commit-msg hook theory was completely wrong.&lt;/p&gt;

&lt;p&gt;Two lessons came out of this. One: "an error message is a suspect, not a conviction — actually run things to isolate the cause." The other concerns cleanup of FAILED markers. The cleanup at the time only "deleted that day's FAILED file on success." The 06-13 FAILED file would keep sitting on my desk the next day and beyond.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 修正後: 過去日の FAILED も正常稼働日に掃く&lt;/span&gt;
find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Desktop/Daily Brief"&lt;/span&gt; &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"FAILED-*.md"&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"FAILED-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TODAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt; &lt;span class="nt"&gt;-delete&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding this one line means that even if failures occur across multiple consecutive days, all the past days' files get cleared on the first day that runs normally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure ②: launchd and a manual run tried to write the vault at the same time (2026-06-10)
&lt;/h3&gt;

&lt;p&gt;One morning, wanting to check on the script's progress, I also ran &lt;code&gt;vault-auto-ingest.sh&lt;/code&gt; manually. launchd's 4:55 slot was already running. Both ran &lt;code&gt;git add -A &amp;amp;&amp;amp; git commit&lt;/code&gt; on the same vault, and a conflict occurred.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;error: cannot lock ref 'refs/heads/main': is at xxx but expected yyy
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the two collided mid-processing, the vault ended up in a half-finished state and needed a manual reset.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;mkdir&lt;/code&gt; lock described earlier solves this, but the key point is "automatic reclamation of stale locks."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; /bin/mkdir &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;oldpid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKDIR&lt;/span&gt;&lt;span class="s2"&gt;/pid"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;oldpid&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$oldpid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[...] 別インスタンス実行中(pid=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;oldpid&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) — skip"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  /bin/mkdir &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKDIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kill -0 "$oldpid"&lt;/code&gt; sends no signal; it only checks whether the process is alive. If the process is dead, it deletes the stale lockdir and takes the lock itself. This prevents the worst case: "a previous abnormal termination leaves the lockdir behind and every subsequent run skips forever."&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure ③: The TCC trap — "it says it succeeded but nothing gets updated"
&lt;/h3&gt;

&lt;p&gt;There was a period when I kept the vault at &lt;code&gt;~/Documents/claude-obsidian/&lt;/code&gt; (later moved to &lt;code&gt;~/claude-obsidian/&lt;/code&gt;, outside TCC protection). The script at the time didn't handle TCC errors properly and exited with &lt;code&gt;exit 0&lt;/code&gt; even when &lt;code&gt;cd "$VAULT"&lt;/code&gt; failed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 修正前の問題コード（イメージ）&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git rev-parse &lt;span class="nt"&gt;--git-dir&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
                                                            ^^^^^^
                                        この &lt;span class="nb"&gt;exit &lt;/span&gt;0 が DONE_MARKER を作る前提を壊す
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the script exits with &lt;code&gt;exit 0&lt;/code&gt;, the calling launchd treats it as "terminated normally." But since the same thing happens on the next run too, all four slots end up in a state of "appearing to succeed while doing nothing," with DONE_MARKER never created.&lt;/p&gt;

&lt;p&gt;The symptoms were strange. The log recorded "===== 完了 =====" four times. But the vault wasn't updated. There was no &lt;code&gt;DONE_MARKER&lt;/code&gt; either.&lt;/p&gt;

&lt;p&gt;I noticed the cause from a message that appeared when I ran the script interactively by hand. When run from launchd, &lt;code&gt;/bin/bash&lt;/code&gt; lacks Full Disk Access, so &lt;code&gt;cd "$VAULT"&lt;/code&gt; was failing silently.&lt;/p&gt;

&lt;p&gt;After the fix, TCC failure is signaled explicitly with &lt;code&gt;exit 1&lt;/code&gt;, and it creates a FAILED file on the desktop plus a notification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git rev-parse &lt;span class="nt"&gt;--git-dir&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;notify_fail &lt;span class="s2"&gt;"FDA未付与: vault にアクセス不可（設定→フルディスクアクセス→/bin/bash）"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1   &lt;span class="c"&gt;# DONE_MARKER は作られない → 次スロットが再試行&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction between &lt;code&gt;exit 0&lt;/code&gt; and &lt;code&gt;exit 1&lt;/code&gt; is what separates "it succeeded" from "the next slot takes over." In an automation context, a silent fake success is the hardest failure mode to diagnose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure ④: Trusting &lt;code&gt;caffeinate -s&lt;/code&gt; too much
&lt;/h3&gt;

&lt;p&gt;The first implementation put only &lt;code&gt;caffeinate -s&lt;/code&gt; at the top of the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 初期の誤った実装&lt;/span&gt;
caffeinate &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-s&lt;/code&gt; flag is described as "prevent system sleep." But in practice &lt;strong&gt;it only works while connected to AC power&lt;/strong&gt;. Close the lid on battery and macOS goes to sleep, freezing the script under &lt;code&gt;caffeinate -s&lt;/code&gt; along with it.&lt;/p&gt;

&lt;p&gt;Checking the next morning, there were traces that the script had started but it had ended in a half-finished state. Because timeout culled it afterward the lockdir was gone, but step2a's marker existed while step2b's did not.&lt;/p&gt;

&lt;p&gt;The fix was to switch to using both &lt;code&gt;-i&lt;/code&gt; (prevent system sleep) and &lt;code&gt;-s&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CAFFEINATED&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; /usr/bin/caffeinate &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nb"&gt;env &lt;/span&gt;&lt;span class="nv"&gt;CAFFEINATED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 /bin/bash &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-i&lt;/code&gt; is effective on battery too. That said, there are situations where lid-close sleep (the suspend equivalent, not display sleep) can't be fully prevented even with &lt;code&gt;-i&lt;/code&gt;. The multi-slot structure — "timeout culls it and the next slot retries the remainder" — is the design that assumes this. caffeinate is only an aid that &lt;em&gt;delays&lt;/em&gt; sleep; what guarantees "the processing will complete" is the combination of markers and multiple slots.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual total of "time spent stuck"
&lt;/h3&gt;

&lt;p&gt;Sorting through these failures, all of them stayed within one day of real damage. The sleep freeze was recovered by the next 8:20 slot. The double-execution race took 10 minutes of manual reset. The all-day outage on 06-13 auto-recovered on the next morning's 4:55 slot. The TCC trap took a few days to notice, but once noticed the fix took under 30 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reason I could notice any of these failures is the logs and the FAILED file on the desktop.&lt;/strong&gt; Had they failed silently, I might not have noticed that the vault hadn't been updated for days. The design of having &lt;code&gt;notify_fail&lt;/code&gt; place a FAILED file on the desktop exists to maximize the chance of noticing. Automation isn't "run it and you're done" — it only really works once you've designed it up through "you'll reliably notice when it breaks." That's my honest takeaway from six months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;p&gt;The earlier sections covered four real failures (all slots dying, the double-execution race, the TCC trap, misplaced faith in caffeinate). Here I'll cover the smaller points that are "not that big, but you will definitely trip on them."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The combination of &lt;code&gt;set -u&lt;/code&gt; and launchd environment variables kills you instantly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The script has &lt;code&gt;set -u&lt;/code&gt; at the top (it's in the actual code). It's the option that makes referencing an undefined variable exit 1 immediately. A shell running from launchd, unlike an interactive shell, has many environment variables undefined. If you reference a variable without giving it a default via the &lt;code&gt;${VAR:-}&lt;/code&gt; form, you get a phenomenon where it's fine in interactive runs but dies only under launchd. In this script, &lt;code&gt;${CAFFEINATED:-}&lt;/code&gt;, &lt;code&gt;${oldpid:-}&lt;/code&gt;, and &lt;code&gt;${net_ok:-}&lt;/code&gt; all explicitly specify an empty default. Miss one and you mass-produce unexplained exit 1s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forget to set &lt;code&gt;RunAtLoad&lt;/code&gt; and it runs at every login&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The plist explicitly has &lt;code&gt;&amp;lt;key&amp;gt;RunAtLoad&amp;lt;/key&amp;gt;&amp;lt;false/&amp;gt;&lt;/code&gt;. Without it the default becomes &lt;code&gt;true&lt;/code&gt;, and the script fires one extra time every time you restart or log in to the Mac. Since it exits immediately when DONE_MARKER exists there's no real harm, but the CPU load right after boot and the unintended log pollution accumulate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without &lt;code&gt;trap&lt;/code&gt;, the lockdir stays behind and every subsequent run skips&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'rm -rf "$LOCKDIR"'&lt;/span&gt; EXIT INT TERM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this one line, the lockdir remains when the script is stopped with &lt;code&gt;Ctrl+C&lt;/code&gt; or &lt;code&gt;kill&lt;/code&gt;. At the next firing, trying to acquire the lock with &lt;code&gt;mkdir&lt;/code&gt; finds the existing lockdir and misjudges it as "another instance running," so it skips. But if you've written code that checks the PID, stale locks are reclaimed automatically. Without stale reclamation, it skips forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running late at night, the date rolls over and two DONE_MARKERs get generated&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the 4:55 slot starts running at 0:10 AM (via a manual start, say), &lt;code&gt;date +%Y%m%d&lt;/code&gt; changes mid-processing and the DONE_MARKER filename changes with it. To prevent this, the script pins the date as of the start of the run with &lt;code&gt;BD="$TODAY"&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TODAY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  &lt;span class="c"&gt;# 日付跨ぎ対策: 評価はラン開始時の日付で固定&lt;/span&gt;
&lt;span class="nv"&gt;ARCH_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;/wiki/briefs/daily/today-brief-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TODAY&lt;/code&gt; is obtained exactly once at the top of the script. Even for long-running processing that crosses midnight, filenames stay unified under "the day the run started."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;stat -f%z&lt;/code&gt; is a macOS-only option&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the log rotation code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;%z &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 5242880 &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On GNU &lt;code&gt;stat&lt;/code&gt; (Linux) it's &lt;code&gt;-c%s&lt;/code&gt;. Since this is written assuming macOS it uses &lt;code&gt;stat -f%z&lt;/code&gt;, but if you try to port it to Linux it silently falls through to &lt;code&gt;echo 0&lt;/code&gt; and never rotates. The &lt;code&gt;|| echo 0&lt;/code&gt; fallback means it doesn't break, but the log keeps growing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without launchd's &lt;code&gt;StandardErrorPath&lt;/code&gt;, stderr vanishes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The plist contains the following.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;StandardErrorPath&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/.../.claude/logs/vault-auto-ingest.launchd.log&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, stderr from processes run via launchd is discarded to the equivalent of &lt;code&gt;/dev/null&lt;/code&gt;. Even if step2's &lt;code&gt;claude -p&lt;/code&gt; is writing something to stderr, you'll never see it. That creates a hard-to-diagnose situation where it's visible in interactive runs and invisible under launchd.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A node-based commit-msg hook dies without PATH augmentation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;git commit&lt;/code&gt; in the vault executes the commit-msg hook. If that hook is written in node, launchd's minimal PATH (roughly &lt;code&gt;/usr/bin:/bin&lt;/code&gt;) has no &lt;code&gt;node&lt;/code&gt;. &lt;code&gt;git commit&lt;/code&gt; stops with a "hook execution failed" and finishes without committing anything. The script auto-discovers nvm's node and adds it to PATH.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NODE_BIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/.nvm/versions/node/&lt;span class="k"&gt;*&lt;/span&gt;/bin 2&amp;gt;/dev/null | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-V&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NODE_BIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_BIN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is taking the tail of a version-order sort with &lt;code&gt;sort -V&lt;/code&gt;. With a string sort you get the inversion where &lt;code&gt;v9.x&lt;/code&gt; sorts after &lt;code&gt;v10.x&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without spelling out "merge into existing articles" in step2b's prompt, you mass-produce duplicate articles&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;step2a (Claude-derived logs) runs first and updates the vault articles. When step2b (Codex-derived logs) runs afterward, it tries to create new articles on the same topics. Without explicit instructions in the prompt, articles with the same content split into &lt;code&gt;projects/foo.md&lt;/code&gt; and &lt;code&gt;projects/foo-2.md&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ingest_src &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STEP2B_MARKER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KB&lt;/span&gt;&lt;span class="s2"&gt;/raw/codex-conversations/"&lt;/span&gt; &lt;span class="s2"&gt;"codex"&lt;/span&gt; 1500 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"Codex由来でも舜の知識として既存記事に統合し、Claude側と重複する話題は新記事を作らず追記でまとめろ。"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This additional prompt (the &lt;code&gt;extra&lt;/code&gt; argument) is passed only to step2b. The instruction to "merge and append" works because it comes after seeing step2a's results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forget to delete the START_STAMP used for the &lt;code&gt;-nt&lt;/code&gt; comparison and the brief is always judged "old"&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;START_STAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; /tmp/vault-ingest-start.XXXXXX&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This temp file is deleted when the script exits. But if it remains in &lt;code&gt;/tmp&lt;/code&gt;, it isn't created under the same name on the next run, and comparison against an old &lt;code&gt;START_STAMP&lt;/code&gt; can judge &lt;code&gt;today-brief.md&lt;/code&gt; as "old," entering a loop where DONE_MARKER is never created. The &lt;code&gt;rm -f "$START_STAMP"&lt;/code&gt; at the end of the script is essential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Marker files left for more than 7 days bloat &lt;code&gt;/tmp&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/logs"&lt;/span&gt; &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'.vault-ingest-*'&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; +7 &lt;span class="nt"&gt;-delete&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave the daily-generated marker files alone and you accumulate 365 files a year in &lt;code&gt;~/.claude/logs/&lt;/code&gt;. &lt;code&gt;-mtime +7&lt;/code&gt; periodically deletes anything older than 7 days. This cleanup line is in there as a memento.&lt;/p&gt;




&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;p&gt;Twelve operating principles that solidified over six months of actually running this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① Separate "success" from "failure that hands off to the next slot" via exit code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the lesson from the TCC incident. Returning &lt;code&gt;exit 0&lt;/code&gt; creates DONE_MARKER and the next slot won't retry. Genuine failures must always be &lt;code&gt;exit 1&lt;/code&gt;. "Silent fake success" is the hardest-to-diagnose form of failure in automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② Create DONE_MARKER only after all steps are complete&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you create DONE_MARKER when step2a completes, "today is done" becomes true even with step2b and the brief unfinished. In this script, DONE_MARKER is created only when the brief is newer than &lt;code&gt;START_STAMP&lt;/code&gt;. The ordering principle: raise the flag only after the final artifact has been produced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ Use intermediate markers to enable partial retry&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rather than a simple two-state "done/not done," give step2a and step2b each a half-marker. If step2a succeeded, the next slot skips step2a and starts from step2b. The more steps there are, the more valuable partial retry becomes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;④ Treat caffeinate as an aid that &lt;em&gt;delays&lt;/em&gt; sleep, and accept that&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even using both &lt;code&gt;-i&lt;/code&gt; and &lt;code&gt;-s&lt;/code&gt;, suspend on a lid-closed MacBook can't be fully prevented. caffeinate is best-effort. What guarantees "it will definitely complete" is markers and multiple slots. A design that leans too hard on caffeinate will always break under battery operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⑤ Derive the number of slots backward from "even the slowest failure finishes within the day"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The four slots (4:55 / 8:20 / 10:45 / 12:15) are designed from the view that "if it finishes by noon, it's usable that day." Even if 4:55 fails, succeeding at 12:15 at worst still gives you a brief based on that day's activity logs. The next morning's firing is for processing the next day's material.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⑥ Always attach &lt;code&gt;--kill-after=30&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run_to&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TIMEOUT_BIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--kill-after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It sends SIGKILL 30 seconds after &lt;code&gt;SIGTERM&lt;/code&gt;. &lt;code&gt;claude -p&lt;/code&gt; sometimes ignores SIGTERM while doing heavy work. Without kill-after, you end up with "the process survives past the timeout while still holding the lockdir."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⑦ Overwrite LLM-output numbers with something deterministic downstream&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;real&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VAULT&lt;/span&gt;&lt;span class="s2"&gt;/wiki"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.md'&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s1"&gt;'*/.*'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"s/総ページ数：[0-9]+/総ページ数：&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;real&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INDEX_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "total page count" Claude writes into &lt;code&gt;wiki/index.md&lt;/code&gt; is a guess. Count the real files and overwrite. Because this runs right before &lt;code&gt;git commit&lt;/code&gt;, the repository always has the measured value committed. The design principle: never treat LLM-generated numbers as authoritative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⑧ Retain the previous value even on fetch failure (last-known-good)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Calendar information has a three-stage fallback: Google Calendar API → icalBuddy → lastgood. Not "it couldn't be fetched, so blank it," but "retain the value from the last success, marked stale." The brief prompt is instructed to call it out explicitly if &lt;code&gt;⚠️stale&lt;/code&gt; is present. Stale information is more useful than none.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⑨ Put failures somewhere visible&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;FAILED_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Desktop/Daily Brief/FAILED-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TODAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt;
notify_fail&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Desktop/Daily Brief"&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"# Daily Brief 生成失敗 — ..."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- 自動再試行: 8:20 / 10:45 / 12:15 ..."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FAILED_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  /usr/bin/osascript &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"display notification ..."&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log files are things you don't notice unless you go look. Putting a FAILED file on the desktop and firing a macOS notification puts "the automation is broken" in front of your eyes. And on success, that file is auto-deleted. A file that "exists only while failing" is proof of normal operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⑩ Clean up past days' FAILED files too&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one line was added while cleaning up after the consecutive 06-13 failures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Desktop/Daily Brief"&lt;/span&gt; &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"FAILED-*.md"&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"FAILED-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TODAY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt; &lt;span class="nt"&gt;-delete&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a design of "delete only that day's file on that day's success," FAILED files keep piling up on your desk after several consecutive days of failure. Design it so that the first normally-operating day deletes all the past days' files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⑪ Auto-discover the nvm node version instead of hardcoding an absolute path&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The launchd plist's EnvironmentVariables hardcodes the &lt;code&gt;v24.13.0&lt;/code&gt; path (line 8 of the reference file). But the node discovery inside the script isn't pinned to an absolute version — it takes the newest via &lt;code&gt;sort -V | tail -1&lt;/code&gt;. If you want to rely on the plist's fixed path, you need to update it every time you run &lt;code&gt;nvm install&lt;/code&gt;. Using the script's dynamic discovery means no changes are needed after a version update. Be conscious of which one you're treating as the source of truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⑫ "It works" and "you'll notice when it breaks" are separate design problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first version only thought as far as "it works." When it broke, I might not have noticed that the vault hadn't been updated for days. Deliberately design multiple detection paths: the FAILED file, the notification, and noticing that the next day's brief is stale. Automation only becomes trustworthy once you've designed it through to "you'll reliably notice when it breaks."&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Let's look back at the triple-layered structure of &lt;code&gt;vault-auto-ingest.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-slot re-firing&lt;/strong&gt; gives you the guarantee of "retrying over the course of a day until it succeeds." The design starts at 4:55 and runs as late as 12:15 in the worst case, but on most days it finishes at 4:55 or 8:20. The remaining two slots become no-ops that check DONE_MARKER and exit immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sleep prevention via &lt;code&gt;caffeinate -i -s&lt;/code&gt;&lt;/strong&gt; is an aid that prevents the process from freezing partway through. It can't fully counter a lid-closed battery-powered Mac, but a frozen run gets culled by timeout and the next slot takes over. The combination of caffeinate and the marker structure establishes a structure where "even if part of it dies, it moves forward."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent retry via step markers&lt;/strong&gt; is the most important part. step2a and step2b each hold independent markers, and completed steps are never re-run. Splitting processing that didn't fit in a 40-minute window into two 25-minute lines means that if one dies, the next slot runs only the rest. This turns "partially timed out" into "partially finished."&lt;/p&gt;

&lt;p&gt;What changed once this ran stably every morning? When Claude Code reads &lt;code&gt;hot.md&lt;/code&gt; the next morning, it can start working already knowing yesterday's decisions, the reason an architecture failed once, and the current state of three projects running in parallel. Explaining everything from scratch every time becomes unnecessary. Management cost doesn't grow even when parallel projects reach three or four.&lt;/p&gt;

&lt;p&gt;The value of automation shows not "when you get it running," but "when it recovers on its own after breaking and keeps running so routinely you don't notice it."&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by **Lily&lt;/em&gt;* — I ship iOS apps and automate my content stack with Claude Code.&lt;br&gt;
Follow along: &lt;a href="https://bokuwalily.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://x.com/bokuwalily" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://github.com/bokuwalily" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>automation</category>
      <category>bash</category>
      <category>macos</category>
      <category>claude</category>
    </item>
    <item>
      <title>My Caption Width Guard Passed Every Test. It Was Measuring Text the Renderer Never Drew.</title>
      <dc:creator>John</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:00:06 +0000</pubDate>
      <link>https://dev.to/hexisteme/my-caption-width-guard-passed-every-test-it-was-measuring-text-the-renderer-never-drew-160d</link>
      <guid>https://dev.to/hexisteme/my-caption-width-guard-passed-every-test-it-was-measuring-text-the-renderer-never-drew-160d</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://hexisteme.github.io/notes/measuring-uppercase-when-render-draws-mixed-case.html" rel="noopener noreferrer"&gt;hexisteme notes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A user complaint sent me into a caption pipeline: "the subtitles cut to two words in places where the sentence doesn't make sense." The fix I shipped for that complaint introduced a second bug, one word narrower and easy to miss, because the code that measured whether a line of text would fit reproduced an assumption about the text that the code drawing the line didn't share. Every test passed the whole time. I only found it by watching the rendered video.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug the complaint pointed at
&lt;/h2&gt;

&lt;p&gt;The captioning system splits a transcript into short chunks that pop onto screen a few words at a time. The chunking function was doing fixed-size slicing — take the next N words, regardless of what came before or after. That's blind to sentence boundaries, so two unrelated sentences could land in the same chunk: &lt;code&gt;loss. Today&lt;/code&gt; reads as one visual unit even though it's the tail of one sentence and the head of the next.&lt;/p&gt;

&lt;p&gt;The fix was a rule set, not a single tweak:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hard break after terminal punctuation (&lt;code&gt;. ! ? …&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;soft break at commas, semicolons, and em-dashes&lt;/li&gt;
&lt;li&gt;extend or push a chunk rather than let it end on a function word (&lt;code&gt;of&lt;/code&gt;, &lt;code&gt;the&lt;/code&gt;, &lt;code&gt;than&lt;/code&gt;, &lt;code&gt;is&lt;/code&gt;, and about thirty others)&lt;/li&gt;
&lt;li&gt;target three words per chunk, four as a ceiling&lt;/li&gt;
&lt;li&gt;a pixel-width cap on the rendered chunk, measured against the actual caption font (Montserrat ExtraBold), with a budget of 1080 × 0.92 = 993.6px&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first four rules are about where a line is allowed to break. The fifth is a physical constraint: however good the break points are, a chunk still has to fit on screen at the font size actually in use. That's the one that went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the width guard actually measured
&lt;/h2&gt;

&lt;p&gt;To get the pixel width of a candidate chunk, the guard rendered the chunk's text through the font and measured the result — which is the correct approach in principle, not a shortcut. Text width isn't a fixed number of pixels per character; it depends on the specific glyphs, so measuring the real string through the real font is the only way to get an honest number.&lt;/p&gt;

&lt;p&gt;The chunk text going into that measurement, though, was uppercased first. Elsewhere in the same video pipeline, an unrelated overlay — hook text shown at the very start of a clip — is deliberately rendered in all caps for a different visual style, and the width-measurement code for the caption line borrowed that same uppercasing step. It reads like a defensible move if you don't check it against what's actually drawn: uppercase glyphs in this font run wider than mixed case, so measuring in uppercase gives you a safety margin — worst case, real width can only come in narrower than what you measured, never wider. Except the caption itself is never rendered in uppercase. The word-pop captions on screen keep the original sentence casing. The margin wasn't a safety margin against a real risk; it was padding for a risk that doesn't exist in this code path, and the padding was large enough to distort the outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement was accurate. The input was wrong.
&lt;/h2&gt;

&lt;p&gt;Take a concrete chunk: "rich with nitrates." Rendered as written, mixed case, it measures 842px. Rendered uppercase — the string the guard actually checked — it measures 1042px, about 20% wider. The budget is 993.6px. The real string clears the budget with room to spare. The string the guard tested against does not.&lt;/p&gt;

&lt;p&gt;So the guard did its job on the input it was given, and the input was wrong. Chunks like "rich" and "with nitrates." got forced apart into one-word fragments to satisfy a width limit that the actual on-screen text was never close to violating. That's a regression relative to the bug this whole rule set was supposed to fix — a one-word chunk is a more broken reading experience than the original two-sentences-in-one-chunk problem, just distributed differently across the caption track.&lt;/p&gt;

&lt;p&gt;It's worth being precise about what was and wasn't broken here, because it would be easy to walk away distrusting the measurement machinery itself. I checked that separately: the library's text-path measurement and the actual rendered glyph width from the graphics library's own &lt;code&gt;get_window_extent()&lt;/code&gt; call agreed to within 1% on the same string (841.6px vs. 849.0px). The measurement tool was fine. The 20% gap came entirely from feeding it a transformed version of the string that the render path never draws.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why green tests didn't catch it
&lt;/h2&gt;

&lt;p&gt;This shipped with every existing test passing. That's not a gap in test coverage in the usual sense — the width-cap logic almost certainly had a unit test asserting that oversized text triggers a split, and that test presumably still passes today, because the function under test does exactly what it's told: given uppercased text and a budget, it correctly decides whether that uppercased text fits.&lt;/p&gt;

&lt;p&gt;The thing no test was checking is whether the uppercased text is the text that gets drawn. That's not a property of the width function in isolation — it's a property of the relationship between two different parts of the pipeline, the measurement path and the render path, and nothing forced those two to agree because nothing compared them. A unit test that only exercises the measurement function, with the transformation already baked into its test fixtures, will happily stay green forever while that transformation drifts away from reality.&lt;/p&gt;

&lt;p&gt;What actually caught this was watching a rendered frame. Not a screenshot diff against a golden image — just a person looking at the output and noticing captions breaking somewhere they shouldn't. There was no automated signal pointing at the bug; there was only the gap between what the pipeline produced and what a human expected to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  What generalizes
&lt;/h2&gt;

&lt;p&gt;The specific failure — uppercasing text before measuring it, then rendering it mixed-case — is narrow. The shape underneath it isn't.&lt;/p&gt;

&lt;p&gt;Any time verification code needs to know something about what a production code path is going to output — its width, its length, its normalized form, its encoding — there are two ways to get that information. One is to call the actual function that the render path calls, so the two are structurally guaranteed to agree. The other is to reimplement an approximation of what that function does, in the verification code, separately. The second option is often faster to write and easier to review in isolation, and it works right up until someone changes an assumption on one side without noticing it needs to change on the other. Case transformation, whitespace normalization, trimming rules, encoding — all of these are exactly the kind of small, easy-to-duplicate, easy-to-forget-you-duplicated logic that drifts.&lt;/p&gt;

&lt;p&gt;Two things reduce the risk, and they're not mutually exclusive. First, share the transformation function itself between the render path and the measurement path, so there's one implementation of "what text actually reaches the screen" instead of two that are supposed to agree. Second — for anything a shared function doesn't cover, or as a backstop even when it does — sample the actual rendered artifact and check it, rather than trusting that a green test suite implies the output looks right. A pipeline nobody looks at is a pipeline where this kind of drift can live for a long time before anyone notices, because nothing in the test suite is positioned to notice it either.&lt;/p&gt;

&lt;p&gt;An earlier note on this site covered the mirror-image failure — &lt;a href="https://hexisteme.github.io/notes/i-confirmed-the-formula-where-both-formulas-agree.html" rel="noopener noreferrer"&gt;a measurement that was accurate but taken at a point where two hypotheses agreed&lt;/a&gt;. This one is closer to home: the measurement tool was accurate too, and the failure was upstream of it, in what got handed to it as input.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;More notes at &lt;a href="https://hexisteme.github.io/notes/" rel="noopener noreferrer"&gt;hexisteme.github.io/notes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>programming</category>
      <category>softwareengineering</category>
      <category>testing</category>
    </item>
    <item>
      <title>ESP32 + Python: From Microcontroller to IoT</title>
      <dc:creator>Janada Pelahara</dc:creator>
      <pubDate>Sun, 23 Aug 2026 23:34:39 +0000</pubDate>
      <link>https://dev.to/janadapelahara/esp32-python-from-microcontroller-to-iot-2498</link>
      <guid>https://dev.to/janadapelahara/esp32-python-from-microcontroller-to-iot-2498</guid>
      <description>&lt;p&gt;&lt;strong&gt;ESP32 + Python: From Microcontroller to IoT&lt;br&gt;
Artcal 0: Introduction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When it comes to transferring data from one place to another, things can sometimes become tricky, especially when communication happens between the hardware and software levels.&lt;/p&gt;

&lt;p&gt;In this article series, I would love to share the experience and knowledge I’ve gathered while working with ESP32 and Python. We’ll explore how these two technologies can work together, starting from the basics and gradually moving towards more interesting and practical projects.&lt;/p&gt;

&lt;p&gt;If you have any questions, suggestions, or ideas along the way, feel free to share them in the comments section below. I’d love to hear from you and discuss them with the community.&lt;/p&gt;

&lt;p&gt;So, without further ado, let’s begin! 🚀&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is ESP32?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Think about Esp32 as a microcontroller with Internet facilities, consisting WiFi, Bluetooth and a own wireless data transfer protocol called ESP-NOW between ESP32 chips. Nowdays, the developers have made development boards integrading these chips for the easy use.&lt;/p&gt;

&lt;p&gt;ESP32 is a family of microcontrollers developed by Espresiff. This can read sensor inputs, process data, contol devices and specially connect to the internet. This is like Arduino but better, faster and smaller. With these information that we have, we can speak about this board as,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"A powerful microcontroller that can interact with electronic components and communicate with other devices through Wi-Fi, Bluetooth, and other communication methods."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Python???&lt;/strong&gt;&lt;br&gt;
We use different languages to tell the same thing but in different ways. We use programming languages to tell the computer the same thing but in different approches. Some languages can be hard to learn and some are easy. Some are well developed and some are not.&lt;/p&gt;

&lt;p&gt;Python programming language was created back in 1980s by Guido Van Rossum, with the development begining around 1989. It was publicly released in Feb, 1991.&lt;/p&gt;

&lt;p&gt;🐍 1989 — Guido van Rossum developing Python.&lt;br&gt;
🐍 1991 — The first public release.&lt;br&gt;
🐍 2000 — Python 2.0.&lt;br&gt;
🐍 2008 — Python 3.0 .&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Python is a high-level, general-purpose programming language known for its simple and readable syntax. This allows programmers to write programs that can perform tasks, process data, and interact with other software, devices, and systems. It is widely used in areas such as web development, automation, data analysis, artificial intelligence, and hardware communication._&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;IoT&lt;/strong&gt;&lt;br&gt;
We interact with objects every day in order to complete our tasks. They all are connected to prefome a task. We're the onece who connect them together.&lt;br&gt;
IoT simply refers to,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Internet of things"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This refers to physical devices that can collect, exchange, and sometimes process data over a network(usually the internet).&lt;/p&gt;

&lt;p&gt;In simple terms, IoT is about connecting everyday objects to the internet so they can connect, communicate and perform tasks automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Combo&lt;/strong&gt;&lt;br&gt;
The combination of these technologies are always like a cup and the tea.&lt;/p&gt;

&lt;p&gt;The combination of ESP32 and Python brings together hardware and software, allowing them to work together to create interactive and connected applications. The ESP32 can collect data from sensors, control electronic components, and communicate over networks, while Python can receive, process, analyze, and display that data. Together, they provide a powerful foundation for building IoT projects and other hardware-software applications.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An ESP32 can read data from a temperature sensor and send that data over Wi-Fi to a computer or a server. Then they can process and get an action with those information as needed. This is an example of an IoT application.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
We learned about ESP32, Python, IoT&lt;br&gt;&lt;br&gt;
In this article, we introduced the two main technologies we’ll be working with throughout this series: ESP32 and Python. We looked at what they are, what they can do, and how combining hardware and software can open up many possibilities for IoT and other practical projects.&lt;/p&gt;

&lt;p&gt;This is only the introduction towards a greater communication. From next article onwards, we’ll start getting our ESP32 and Python knowledge with theory &amp;amp; practical examples. This is just our first steps toward making them communicate with each other.&lt;/p&gt;

&lt;p&gt;We'll meeeeet soooooooooooon! 🚀&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Written By Janada Pelahara&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>esp32</category>
      <category>python</category>
      <category>programming</category>
      <category>iot</category>
    </item>
    <item>
      <title>99% token accuracy, zero learning. Field notes from fine-tuning vision models with RL.</title>
      <dc:creator>Rickesh T N</dc:creator>
      <pubDate>Sun, 23 Aug 2026 23:33:17 +0000</pubDate>
      <link>https://dev.to/rickeshtn/99-token-accuracy-zero-learning-field-notes-from-fine-tuning-vision-models-with-rl-306l</link>
      <guid>https://dev.to/rickeshtn/99-token-accuracy-zero-learning-field-notes-from-fine-tuning-vision-models-with-rl-306l</guid>
      <description>&lt;p&gt;Over the past year I have been fine-tuning open vision-language models - 9B dense up to a 35B mixture-of-experts - with supervised fine-tuning and GRPO-style reinforcement learning on verifiable rewards. Most of what I learned was not about algorithms. It was about the ways a training run can look healthy while doing nothing, or crash for reasons that have nothing to do with your code.&lt;/p&gt;

&lt;p&gt;Three failures, in increasing order of how long they fooled me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 1: the metric that measured the wrong thing (18 hours)
&lt;/h2&gt;

&lt;p&gt;I ran an 18-hour supervised fine-tune that reported token accuracy climbing steadily to 99%. Looked like a textbook run. The real evaluation metric - accuracy on multiple-choice questions - never moved.&lt;/p&gt;

&lt;p&gt;The cause was a mismatch between what I supervised and what I evaluated. The training loss was over free-text reasoning traces; the evaluation scored a single extracted answer letter. The model got extremely good at reproducing the &lt;em&gt;shape&lt;/em&gt; of the training text - hence 99% token accuracy - without that transferring to the decision I actually cared about.&lt;/p&gt;

&lt;p&gt;Token accuracy is a proxy, and proxies drift from the target exactly when you stop checking. The fix was structural, not a hyperparameter: supervise the thing you evaluate. If the deliverable is a constrained answer, the training signal has to reach that answer, not just the prose around it.&lt;/p&gt;

&lt;p&gt;The general rule I took: &lt;strong&gt;any training metric that is not your evaluation metric is a hypothesis about correlation, and you should check that correlation before you spend GPU-days on it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 2: the crash that was two libraries disagreeing about position ids
&lt;/h2&gt;

&lt;p&gt;The GRPO trainer for the 9B vision model crashed in the forward pass, deep inside rotary position embedding code. Nothing in my training code had changed.&lt;/p&gt;

&lt;p&gt;The diagnosis took a while because the bug lived at the boundary between components: the text sequence length was derived from token-type ids, while the vision sequence length came from the image grid - and image-pad tokens ended up counted twice. Two parts of the same stack, each internally consistent, disagreeing about how long the input was.&lt;/p&gt;

&lt;p&gt;For the 35B MoE variant of the same family, an equivalent rope bug was fixable by monkeypatching the model's position-id computation. I shipped the patch with a &lt;strong&gt;GPU-free regression test&lt;/strong&gt;: a tiny script that constructs the exact failing input shape and runs just the position-id path on CPU. It runs in seconds, needs no cluster, and fails loudly if an upstream update reintroduces the bug.&lt;/p&gt;

&lt;p&gt;Two lessons. First, when you fine-tune at the edge of a model family's tooling support, the bugs you hit are integration bugs, and the stack trace points at the victim, not the culprit. Second, every monkeypatch deserves a regression test that costs nothing to run - otherwise the next library upgrade silently un-fixes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 3: the RL loop that was learning the opposite (the quiet one)
&lt;/h2&gt;

&lt;p&gt;In a separate project I fine-tune a 9B model with reinforcement learning where the reward comes from realized real-world outcomes rather than a labelled dataset. For a long stretch the training signal was flat - not diverging, not collapsing, just flat, which is the least informative failure there is.&lt;/p&gt;

&lt;p&gt;Two compounding problems. One was label noise in the reward pipeline: some outcomes were being attributed to the wrong decisions, which dilutes any gradient. The other was worse: a sign error meant part of the advantage signal was inverted. The model was being gently pushed &lt;em&gt;away&lt;/em&gt; from behaviour that had worked.&lt;/p&gt;

&lt;p&gt;Nothing crashed. Every batch processed. Every log line looked like a training run. The only symptom was the absence of learning, and the only way I found it was working backwards from "the held-out metric should have moved by now" to auditing every stage of the reward computation by hand.&lt;/p&gt;

&lt;p&gt;After both fixes I got the first genuinely monotonic learning curve on that task. I still treat it as training signal only - the decider is held-out evaluation against the base model, and I do not report improvements that exist only in the training curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The harness rules I now run everything under
&lt;/h2&gt;

&lt;p&gt;These came out of the failures above plus a benchmarking programme across 70+ vision-language models. They are boring, and they are the difference between numbers and noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smoke test before committing compute.&lt;/strong&gt; A five-step GRPO run with two numbers watched: the PPO-style clip ratio and the fraction of outputs that parse. If the clip ratio is degenerate or parseability is low, the full run will be garbage in a way five steps already reveals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runs are gated fail-closed.&lt;/strong&gt; Nothing publishes a result unless the evaluation stage actually scored. "The eval crashed but training finished" is not a result; it is an unscored run, and unscored runs must be impossible to mistake for scored ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure failures and poor performance are different columns.&lt;/strong&gt; An unparseable output, an OOM, a crashed kernel - these are exceptions whose count must be exactly zero. A weak model produces zero exceptions and simply scores badly. If a quality threshold can absorb an infrastructure failure, a totally broken run can pass your gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The held-out metric is the only decider.&lt;/strong&gt; Training curves, token accuracy, reward trends - all of it is telemetry. If the held-out number did not move, nothing happened.&lt;/p&gt;

&lt;p&gt;None of this is novel. All of it is the difference between the runs I trust and the 18 hours I lost.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about ML evaluation, world models, and the ways measurement quietly fails. More at &lt;a href="https://dev.to/rickeshtn"&gt;dev.to/rickeshtn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>llm</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Log Level Design and Rotation: Why Apps Use DEBUG/INFO/WARNING/ERROR</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Sun, 23 Aug 2026 23:32:27 +0000</pubDate>
      <link>https://dev.to/susumun/log-level-design-and-rotation-why-apps-use-debuginfowarningerror-4bdi</link>
      <guid>https://dev.to/susumun/log-level-design-and-rotation-why-apps-use-debuginfowarningerror-4bdi</guid>
      <description>&lt;p&gt;Open any application log and you'll see the same kind of message tagged with different labels: &lt;code&gt;DEBUG&lt;/code&gt;, &lt;code&gt;INFO&lt;/code&gt;, &lt;code&gt;WARNING&lt;/code&gt;, &lt;code&gt;ERROR&lt;/code&gt;. Why not just write down everything that happens, in one uniform stream? This post looks at what log levels actually do, and at the companion problem every long-running app eventually faces: keeping a log file from growing forever (rotation).&lt;/p&gt;

&lt;h2&gt;
  
  
  A Log Level Is a Filtering Threshold
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: logging means recording what happened while a program runs, so it can be reviewed later in a file or on screen.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Python's standard &lt;code&gt;logging&lt;/code&gt; module defines five levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Numeric value&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DEBUG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Fine-grained detail for tracing exactly what the code did&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INFO&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Normal progress record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WARNING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;Something unexpected, but processing continues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ERROR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;An operation actually failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CRITICAL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;The application itself can no longer continue&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key point is that this number isn't just a label — it's a threshold used for filtering. Set &lt;code&gt;logger.setLevel(logging.INFO)&lt;/code&gt; and only messages at 20 or above (INFO, WARNING, ERROR, CRITICAL) get written out; anything at DEBUG (10) is silently dropped. In other words, level design isn't only about deciding what to record — it's about being able to dial the visible detail up or down later without touching the code. In normal operation you watch INFO and above; when something goes wrong, you temporarily drop the threshold to DEBUG to see the fine-grained trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This App Actually Configures It
&lt;/h2&gt;

&lt;p&gt;At the top of &lt;code&gt;maintenance_agent.py&lt;/code&gt;, log output is routed to two handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_rotating_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RotatingFileHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maintenance.log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;maxBytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 10 MB
&lt;/span&gt;    &lt;span class="n"&gt;backupCount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_stream_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StreamHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_rotating_handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_stream_handler&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the threshold is &lt;code&gt;logging.INFO&lt;/code&gt;, DEBUG-level messages produce no output during normal operation. Across the codebase there are 19 &lt;code&gt;logger.debug()&lt;/code&gt; calls — statements written to stay quiet by default and only become useful once someone deliberately lowers the threshold during an investigation. That's the practical payoff of level filtering: you can leave detailed diagnostic statements embedded in the code permanently, without needing to add them later, and without them cluttering the log under normal conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Actual Level Distribution Reveals
&lt;/h2&gt;

&lt;p&gt;Counting calls across the same codebase gives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;INFO&lt;/code&gt;: 135 calls&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WARNING&lt;/code&gt;: 114 calls&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ERROR&lt;/code&gt;: 28 calls&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CRITICAL&lt;/code&gt;: 0 calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;INFO dominates because the app processes multiple WordPress sites in sequence during a maintenance run, and each step of that sequence needs a progress record. WARNING is the next largest category, and a good example is in &lt;code&gt;core/alert_utils.py&lt;/code&gt;, where &lt;code&gt;send_alert_email()&lt;/code&gt; detects incomplete SMTP settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;smtp_host&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;smtp_user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;to_email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]):&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;メール送信設定が不足しているためスキップします。&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Email settings incomplete. Skipping notification.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice this uses &lt;code&gt;logger.warning&lt;/code&gt;, not &lt;code&gt;logger.error&lt;/code&gt;. Even if the notification email can't be sent, the rest of the maintenance run — backups, updates, rollback decisions — can still proceed; nothing has actually failed. &lt;code&gt;ERROR&lt;/code&gt; is reserved for cases where an operation genuinely did fail (a WP-CLI update command erroring out, an exception during mail delivery, and similar), which is why it's the smallest of the three at 28 occurrences.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CRITICAL&lt;/code&gt; doesn't appear at all, and that's not an oversight — it follows from how the app is designed. Each site is processed independently: if one site's maintenance run fails, that site alone gets rolled back and the run moves on to the next site. There's essentially no scenario where the whole application needs to be treated as unable to continue. On top of that, urgent notifications to the user aren't handled through the log level at all — they go through a separate channel, &lt;code&gt;send_alert_email()&lt;/code&gt;. Deciding what gets written to the log and deciding what the user needs to be told are two different concerns, and the second one lives in application logic, not in log-level plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotation: Making "Keep Recording Forever" Safe
&lt;/h2&gt;

&lt;p&gt;Logs get more useful the longer they accumulate, but an unbounded log file will eventually fill the disk. &lt;code&gt;RotatingFileHandler&lt;/code&gt; solves this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_rotating_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RotatingFileHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maintenance.log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;maxBytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 10 MB
&lt;/span&gt;    &lt;span class="n"&gt;backupCount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;maxBytes=10 * 1024 * 1024&lt;/code&gt; triggers a rotation once the file reaches 10MB; &lt;code&gt;backupCount=5&lt;/code&gt; caps how many rotated generations are kept. When the active file hits the size limit, it's renamed &lt;code&gt;maintenance.log.1&lt;/code&gt; and a fresh empty file takes over. The next time the limit is hit, &lt;code&gt;maintenance.log.1&lt;/code&gt; becomes &lt;code&gt;maintenance.log.2&lt;/code&gt;, and so on — once a generation would exceed &lt;code&gt;backupCount&lt;/code&gt;, it's deleted.&lt;/p&gt;

&lt;p&gt;That fixes the maximum disk footprint at &lt;code&gt;10MB × (1 + backupCount)&lt;/code&gt; — about 60MB in this app's case — no matter how long the process keeps running. For a desktop app meant to stay running over long periods, this single mechanism satisfies two requirements at once: the log can never grow without bound, and a meaningful amount of recent history is still available whenever something needs investigating.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Second Handler for Logs That Shouldn't Persist
&lt;/h2&gt;

&lt;p&gt;There's one more custom handler in this app, &lt;code&gt;_SiteLogCapture&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_SiteLogCapture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Temporarily captures the log output for a single site&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s run.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its purpose is entirely different from the rotating file handler. It gets attached to the logger only while one site's maintenance run is in progress, collecting just that run's log lines into an in-memory list. Once the run finishes, that captured text becomes the "execution log" section of the white-label report or notification email, and the handler is detached again.&lt;/p&gt;

&lt;p&gt;Two handlers can be attached to the same logger and treat the exact same stream of messages completely differently. The rotating handler exists to preserve the app's history long-term under a fixed size cap; &lt;code&gt;_SiteLogCapture&lt;/code&gt; exists to gather one run's output ephemerally, for one specific downstream use, and then discard it. If log levels are the "vertical" filter — how much detail to keep — handler choice is the "horizontal" filter: which audience, and which purpose, a given record ends up serving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A log level isn't a static classification tag; it's a threshold you can adjust after the fact. DEBUG stays hidden by default, INFO records normal progress, WARNING marks recoverable anomalies, ERROR marks actual failures, and CRITICAL marks a failure the application itself cannot survive — choosing deliberately among them means the resulting distribution of log calls ends up reflecting the application's actual design assumptions. Rotation, meanwhile, is the practical mechanism that lets "keep recording indefinitely" and "don't run out of disk space" coexist, simply by fixing a size and a generation count up front.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
