<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Typst on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/tags/typst/</link><description>Recent content in Typst on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 11 May 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/tags/typst/index.xml" rel="self" type="application/rss+xml"/><item><title>hybrid-image-search dev log #19 — Merged model pool of 246, admin gates, gpt-image-2 resolution, and Phase 1 error handling</title><link>https://ice-ice-bear.github.io/posts/2026-05-11-hybrid-search-dev19/</link><pubDate>Mon, 11 May 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/posts/2026-05-11-hybrid-search-dev19/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post hybrid-image-search dev log #19 — Merged model pool of 246, admin gates, gpt-image-2 resolution, and Phase 1 error handling" /&gt;&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;If &lt;a class="link" href="https://ice-ice-bear.github.io/posts/2026-05-07-hybrid-search-dev18/" &gt;#18&lt;/a&gt; routed OpenAI in as Side B, #19 is the cycle that smooths over the consequences of that decision. 21 commits, five PRs (#20–#24), and on the last day a Typst PDF error report built from Grafana Cloud Loki logs that drove the final code change.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Start["dev #18 (c43214e)"] --&gt; Eval["Search eval harness &amp;lt;br/&amp;gt; offline baseline"]
 Eval --&gt; Pool["Model pool 0428/0504 merge &amp;lt;br/&amp;gt; 142 → 246"]
 Pool --&gt; ModelUX["Model UX &amp;lt;br/&amp;gt; mode preservation, 16:9 crop, base force-Edit"]
 ModelUX --&gt; Admin["Admin &amp;lt;br/&amp;gt; activity log modal, Nano gate"]
 Admin --&gt; ResQuality["gpt-image-2 resolution/quality &amp;lt;br/&amp;gt; user input passthrough"]
 ResQuality --&gt; Phase1["Phase 1 error handling &amp;lt;br/&amp;gt; Typst report → global deadline"]
 Phase1 --&gt; End["dev #19 (e09036d)"]&lt;/pre&gt;&lt;p&gt;The big question this cycle: &lt;strong&gt;&amp;ldquo;When Side B starts failing in production, what do you retry and what do you give up on quickly?&amp;rdquo;&lt;/strong&gt; The answer landed most clearly in the last commit.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="search-eval-harness-rejecting-top_k_fusion64"&gt;Search eval harness: rejecting top_k_fusion=64
&lt;/h2&gt;&lt;p&gt;The first cluster of commits set up evaluation infrastructure for the search side. Until now, reranker tweaks and fusion parameters were tested live in production — strong qualitative impressions, no quantitative numbers.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 Query["query set &amp;lt;br/&amp;gt; (curated)"] --&gt; Fusion["RRF fusion &amp;lt;br/&amp;gt; (top_k_fusion candidates)"]
 Fusion --&gt; Rerank["bge-reranker &amp;lt;br/&amp;gt; (top_k_rerank)"]
 Rerank --&gt; Eval["offline harness &amp;lt;br/&amp;gt; recall@5, mrr@10"]
 Eval --&gt; Baseline["2026-05-07 baseline JSON"]&lt;/pre&gt;&lt;p&gt;Key commits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;feat(eval): offline search-quality harness + 2026-05-07 baseline&lt;/code&gt;&lt;/strong&gt; — query set + ground truth + RRF→rerank pipeline wired into a CLI. Baseline JSON committed to the repo as a benchmark for future comparisons.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;docs(search): top_k_fusion=64 evaluated and rejected — eval harness wins&lt;/code&gt;&lt;/strong&gt; — Intuition said a wider fusion candidate set should help, so 64 got tried. The harness measured +0.2% gain. The cost (reranker GPU time +30%) made it pointless. &lt;strong&gt;First case where the harness overrode intuition&lt;/strong&gt; — documented in &lt;code&gt;docs/decisions/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;feat(search): request-level OTel span attrs + reranker-doc cleanup&lt;/code&gt;&lt;/strong&gt; — Attached query, fusion candidates, rerank scores as span attrs in tracing. This becomes the analysis substrate for the next cycle.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="modal-portaling-pinning-positionfixed-to-the-viewport"&gt;Modal portaling: pinning &lt;code&gt;position:fixed&lt;/code&gt; to the viewport
&lt;/h2&gt;&lt;p&gt;A tiny CSS bug with a surprising side effect. Modals lived inside a parent with &lt;code&gt;transform: ...&lt;/code&gt; applied, and per CSS spec, a transformed parent becomes the containing block for &lt;code&gt;position: fixed&lt;/code&gt; children. So the modal was anchored to the parent&amp;rsquo;s box, not the viewport.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// before — modal rendered inside ImagePanel
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;ImagePanel() {&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;translateZ(0)&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}}&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* forces GPU layer */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showModal&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;Modal&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// after — Portal to body
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createPortal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kr"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;react-dom&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;ImagePanel() {&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;translateZ(0)&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}}&amp;gt;...&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showModal&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;createPortal&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;Modal&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Commit message reads exactly that: &lt;code&gt;fix: portal modals to body so position:fixed pins to viewport&lt;/code&gt;. Looked like one line, but pulled along two side effects — z-index reset and outside-click detection logic.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="model-pool-merging-0428-back-into-0504-142--246"&gt;Model pool: merging 0428 back into 0504 (142 → 246)
&lt;/h2&gt;&lt;p&gt;At the end of #18 the 0428 pool got reseeded to 0504 (with folder-hint labels). The April 28 catalog was swapped for the May 4 one. User feedback came fast — &amp;ldquo;Models I&amp;rsquo;d been using from the old pool disappeared.&amp;rdquo;&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Pool0428["0428 pool &amp;lt;br/&amp;gt; 142 models"] --&gt; Reseed["dev #18: reseed &amp;lt;br/&amp;gt; to 0504"]
 Reseed --&gt; Pool0504["0504 pool &amp;lt;br/&amp;gt; ~146 models"]
 Pool0428 -- "merge back" --&gt; Merged["merged pool &amp;lt;br/&amp;gt; 246 models"]
 Pool0504 --&gt; Merged
 Merged --&gt; Picker["model-picker &amp;lt;br/&amp;gt; dedupe + filter exhaustion"]&lt;/pre&gt;&lt;p&gt;Two commits closed the loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;feat(models): merge 0428 pool back into 0504 model pool (142 -&amp;gt; 246)&lt;/code&gt;&lt;/strong&gt; — Combined so users keep access to both. After dedupe, settled at 246.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;feat(model-picker): dedupe re-picks and surface filter exhaustion&lt;/code&gt;&lt;/strong&gt; — Same user never gets the same model twice in a row. Filters can narrow so far that no candidates match — the UI now surfaces &amp;ldquo;No more candidates — try loosening filters.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="model-ux-mode-preservation-169-crop-base-force-edit"&gt;Model UX: mode preservation, 16:9 crop, base force-Edit
&lt;/h2&gt;&lt;p&gt;PRs #20–#22 grouped here. Three calls:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(1) Keep generation mode when closing the library panel.&lt;/strong&gt; Before this, closing the library reset mode to &lt;code&gt;auto&lt;/code&gt;. Users said: &amp;ldquo;I explicitly chose Edit — why does closing the panel revert it?&amp;rdquo; Explicit choices should only be undone by explicit actions.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// frontend/lib/state.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;closeLibrary&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;setLibraryPanelCollapsed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;setActiveLibraryTab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;resetInjectionMode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ← removed
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;closeLibrary&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;setLibraryPanelCollapsed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;setActiveLibraryTab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A separate commit (&lt;code&gt;fix(generation): reset injection mode to auto when closing library panel&lt;/code&gt;) then made the reset explicit for the specific case where the panel is closed via the close button. Two commits framing the same decision from both sides — remove the unintentional reset, make the intentional one explicit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(2) gpt-image-2 16:9 crop.&lt;/strong&gt; gpt-image-2 only outputs &lt;code&gt;1024x1024&lt;/code&gt;, &lt;code&gt;1024x1536&lt;/code&gt;, or &lt;code&gt;1536x1024&lt;/code&gt;. A user request for 16:9 returns 1536x1024. The UI now renders the prediction box at 16:9 and center-crops the result to 16:9 for display.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(3) &amp;ldquo;Base&amp;rdquo; button forces Edit mode.&lt;/strong&gt; On the detail screen, the base-model button must enter Edit mode (not inherit from source). All other paths (auto-injection, model picker) still enter &lt;code&gt;auto&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The follow-up commit &lt;code&gt;fix(generation): tighten model auto-injection + require base for Edit mode&lt;/code&gt; enforces this all the way down: Edit-mode requests without a base image are rejected with 422.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="admin-activity-log-modal-nano-only-gate"&gt;Admin: activity log modal, Nano Only gate
&lt;/h2&gt;&lt;p&gt;PRs #21 and #24 covered internal-ops features.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Activity log modal&lt;/strong&gt; — Admins can view/download recent generate calls for a specific user. Essential for debugging and beta tester support.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 Admin["admin page &amp;lt;br/&amp;gt; user search"] --&gt; Modal["activity log modal"]
 Modal --&gt; View["view mode &amp;lt;br/&amp;gt; last N calls"]
 Modal --&gt; Download["download mode &amp;lt;br/&amp;gt; JSONL export"]
 View --&gt; Anon["PII masking &amp;lt;br/&amp;gt; no image URLs"]&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Nano Only mode&lt;/strong&gt; — New admin allowlist pattern. A specific admin user (&lt;code&gt;khk@diffs.studio&lt;/code&gt;) gets a &amp;ldquo;Nano Only&amp;rdquo; mode that calls only the smaller, cheaper model. Production cost guard rail + safe mode for demos.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="gpt-image-2-resolutionquality-passthrough"&gt;gpt-image-2 resolution/quality passthrough
&lt;/h2&gt;&lt;p&gt;Today&amp;rsquo;s first commit (2026-05-11) is small but had outsized production impact.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;feat(generation): pass user resolution + quality to gpt-image-2&lt;/code&gt; — until now the backend silently ignored user-selected resolution/quality and called gpt-image-2 with default &lt;code&gt;1024x1024&lt;/code&gt;, &lt;code&gt;quality=auto&lt;/code&gt;. The user picks &amp;ldquo;high-res 16:9&amp;rdquo; and gets a 1024 square. The UI had setters; the backend wiring was missing.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# backend/openai_service.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_pick_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aspect&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1024x1024&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# always square ← placeholder that got stuck&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_pick_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aspect&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;requested_quality&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="c1"&gt;# gpt-image-2 hard-limits output to 1024x1024 / 1024x1536 / 1536x1024&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="c1"&gt;# (max ~3:2). The size mapper picks the closest valid output.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;aspect&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;16:9&amp;#34;&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;aspect&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;21:9&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1536x1024&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;aspect&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;9:16&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1024x1536&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1024x1024&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;b5a0ede — fix(generation-feed): keep each card at its A-image's natural aspect&lt;/code&gt; belongs to the same theme — the generation feed grid now follows Side A&amp;rsquo;s (Gemini&amp;rsquo;s, more flexible) aspect ratio per card.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="phase-1-error-handling-grafana-loki--typst--code-decision"&gt;Phase 1 error handling: Grafana Loki → Typst → code decision
&lt;/h2&gt;&lt;p&gt;The most interesting thread in this cycle was the last session (109 min, &lt;code&gt;1358feee&lt;/code&gt;). Pull seven days of image generation error logs from Grafana Cloud Loki, build a Typst PDF report, then change code &lt;strong&gt;based on what the report recommended.&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
 Loki["Grafana Cloud Loki &amp;lt;br/&amp;gt; service_name=hybrid-image-search"] --&gt; Tally["Error category tally &amp;lt;br/&amp;gt; 6 categories"]
 Tally --&gt; Report["docs/error-report.typ &amp;lt;br/&amp;gt; share, retriability, recs"]
 Report --&gt; Decision["Retry everything? Or focus?"]
 Decision --&gt; Phase1["Phase 1 only &amp;lt;br/&amp;gt; (low-risk items)"]
 Decision --&gt; Defer["Phase 2-1 deferred &amp;lt;br/&amp;gt; (Gemini/OpenAI retry)"]
 Phase1 --&gt; Code["e09036d &amp;lt;br/&amp;gt; global deadline + explicit error classification"]&lt;/pre&gt;&lt;p&gt;A single user question in the middle of the report shaped the final call:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;&amp;ldquo;If we just blanket-apply retry logic, wouldn&amp;rsquo;t it cascade into the same time-window&amp;rsquo;s other image generation calls?&amp;rdquo;&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;That insight went into report v2. If #1 (Gemini 503 retry) and #2 (OpenAI&amp;rsquo;s own retry) both kick in, and then #3 (Gemini → OpenAI fallback) on top, a single user could in the worst case hit the multiplied retries of two APIs at the same time. That looks like a thundering herd — the bad-minute&amp;rsquo;s throughput collapses on itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decision: Phase 1 only. Low-risk items — explicit error classification + global deadline.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# backend/service.py — global deadline pattern&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_with_deadline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deadline_s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;60.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_generate_inner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;deadline_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;GenerationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;timeout&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;retriable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# ← user retries with a fresh request&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Image generation exceeded 60s deadline&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;gemini&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServerError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# 503-class&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;GenerationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;upstream-503&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retriable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;APIError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;GenerationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;openai-api&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retriable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Intentional design choice&lt;/strong&gt;: every classified error is &lt;code&gt;retriable=False&lt;/code&gt;. The backend doesn&amp;rsquo;t retry; the user explicitly resubmits. That&amp;rsquo;s the safety boundary for phase 1. Phase 2&amp;rsquo;s decision — which specific categories get auto-retry restored — waits on 1-2 more weeks of Loki data.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="commit-log"&gt;Commit log
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Message&lt;/th&gt;
 &lt;th&gt;Area&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;chore: reseed model pool from 0428 to 0504 with folder-hint labels&lt;/td&gt;
 &lt;td&gt;data/model_pool/*.json&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix: portal modals to body so position:fixed pins to viewport&lt;/td&gt;
 &lt;td&gt;frontend/components/Modal.tsx&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(search): request-level OTel span attrs + reranker-doc cleanup&lt;/td&gt;
 &lt;td&gt;backend/search/*.py, observability&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(eval): offline search-quality harness + 2026-05-07 baseline&lt;/td&gt;
 &lt;td&gt;scripts/eval/, eval/baselines/*.json&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;docs(search): top_k_fusion=64 evaluated and rejected — eval harness wins&lt;/td&gt;
 &lt;td&gt;docs/decisions/&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(models): merge 0428 pool back into 0504 model pool (142 -&amp;gt; 246)&lt;/td&gt;
 &lt;td&gt;data/model_pool/&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(ui): mode preservation, larger model preview, GPT 16:9 crop, model name in detail&lt;/td&gt;
 &lt;td&gt;frontend (PR #20)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(admin): user activity log modal with view/download&lt;/td&gt;
 &lt;td&gt;backend/admin/, frontend/admin/ (PR #21)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(model-picker): dedupe re-picks and surface filter exhaustion&lt;/td&gt;
 &lt;td&gt;frontend/components/ModelPicker.tsx&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(generation): reset injection mode to auto when closing library panel&lt;/td&gt;
 &lt;td&gt;frontend/lib/state.ts&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(detail): base button forces Edit mode instead of inheriting source mode&lt;/td&gt;
 &lt;td&gt;frontend (PR #22)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(generation): tighten model auto-injection + require base for Edit mode&lt;/td&gt;
 &lt;td&gt;backend/generation/, frontend (PR #23)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(admin): Nano Only mode + add &lt;a class="link" href="mailto:khk@diffs.studio" &gt;khk@diffs.studio&lt;/a&gt; to admin allowlist&lt;/td&gt;
 &lt;td&gt;backend/auth/, admin (PR #24)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(generation): pass user resolution + quality to gpt-image-2&lt;/td&gt;
 &lt;td&gt;backend/openai_service.py&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(generation-feed): keep each card at its A-image&amp;rsquo;s natural aspect&lt;/td&gt;
 &lt;td&gt;frontend/components/GenerationFeed.tsx&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(generation): harden error handling (Phase 1 + global deadline)&lt;/td&gt;
 &lt;td&gt;backend/service.py, docs/error-report.typ&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="insights"&gt;Insights
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;(1) An eval harness with a baseline beats intuition.&lt;/strong&gt; The top_k_fusion=64 rejection is the smallest code change in #19 (a single docs file) but the biggest process change. From now on, any search-side parameter tweak gets measured against the baseline JSON before landing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(2) Modal portaling is the kind of small CSS defect that only surfaces with production data.&lt;/strong&gt; Adding &lt;code&gt;transform: translateZ(0)&lt;/code&gt; to force a GPU layer wasn&amp;rsquo;t wrong on its own. But that decision quietly changed &lt;code&gt;position: fixed&lt;/code&gt;&amp;rsquo;s containing block — a fact invisible in React DevTools, only visible when a real browser puts the modal in the wrong spot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(3) Grafana Loki → Typst → code-decision flow turned out unexpectedly strong.&lt;/strong&gt; Normally I look at the dashboard and patch what&amp;rsquo;s broken. This cycle, seven days of logs got categorized, rendered to PDF, and the code change was driven by the report&amp;rsquo;s recommendation. The act of writing the report became the design doc — &amp;ldquo;Phase 1 only, Phase 2 deferred&amp;rdquo; lives in the report body.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(4) The first rule of production error response is don&amp;rsquo;t blindly add retries.&lt;/strong&gt; One retry feels safe; two retries multiplying turns into a thundering herd. A single user-posed question pulled the conclusion out into the open.&lt;/p&gt;
&lt;p&gt;Next cycle #20 picks up Phase 2 — after 1-2 weeks of Loki data on the pure Gemini 503 rate, decide which categories get selective auto-retry.&lt;/p&gt;</description></item></channel></rss>