[{"data":1,"prerenderedAt":328},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-breakpoint-on-the-wrong-block-en":72},{},{"featuredPost":5,"navigations":29},{"id":6,"title":7,"title_id":8,"slug":9,"excerpt":10,"excerpt_id":11,"has_translation":12,"published_at":13,"reading_time":14,"hidden_from_listing":15,"tags":16,"thumbnail_image":21,"og_image":22,"series":23,"authors":28},110,"When You Pick a Framework Just to Look Different","Ketika Pilih Framework Cuma Biar Kelihatan Beda","ketika-pilih-framework-cuma-biar-kelihatan-beda","The last time you chose a new framework or library, was it because your problem needed that tool, or because it sounded cool and few people around you were using it? That question is uncomfortable. Technology decisions are often made for reasons that never appear in any technical document.","Terakhir kali kamu milih framework atau library baru, itu karena masalahmu butuh alat itu, atau karena alatnya kedengaran keren dan belum banyak yang pakai? Pertanyaan ini nggak enak dijawab. Soalnya keputusan teknologi sering diambil dengan alasan yang nggak pernah ditulis di dokumen teknis mana pun.",true,"2026-10-06T15:25:21.000Z",5,false,[17,18,19,20],"karier","framework","opini","pengalaman",null,"\u002Fog\u002Fketika-pilih-framework-cuma-biar-kelihatan-beda.png",{"id":24,"title":25,"title_id":26,"slug":27},9,"Articles","Artikel","all",[],[30,46,67],{"collection":31,"item":32},"series",{"__typename":31,"title":25,"title_id":26,"slug":27,"children_series":33},[34,38,42],{"title":35,"title_id":36,"slug":37},"Artificial Intelligence","Kecerdasan Artifisial","kecerdasan-artifisial",{"title":39,"title_id":40,"slug":41},"Programming Fundamentals","Konsep Dasar Pemrograman","konsep-dasar-pemrograman",{"title":43,"title_id":44,"slug":45},"Algorithms and Data Structures","Algoritma dan Struktur Data","algoritma-struktur-data",{"collection":31,"item":47},{"__typename":31,"title":48,"title_id":48,"slug":49,"children_series":50},"Tutorial","tutorial",[51,55,59,63],{"title":52,"title_id":53,"slug":54},"Learning Python","Belajar Python","belajar-python",{"title":56,"title_id":57,"slug":58},"Learning Go","Belajar Golang","belajar-golang",{"title":60,"title_id":61,"slug":62},"Learning Java","Belajar Java","belajar-java",{"title":64,"title_id":65,"slug":66},"Learning Pascal","Belajar Pascal","belajar-pascal",{"collection":31,"item":68},{"__typename":31,"title":69,"title_id":69,"slug":70,"children_series":71},"Lab","lab",[],{"article":73,"renderedBody":202,"renderedTakeaway":203,"latestArticles":204,"toc":308},{"id":74,"title":75,"title_id":76,"slug":77,"excerpt":78,"excerpt_id":79,"has_translation":12,"published_at":80,"reading_time":81,"hidden_from_listing":12,"tags":82,"thumbnail_image":21,"og_image":86,"series":87,"authors":193,"seoTitle":21,"seoDescription":21,"body_locale":196,"available_locales":197,"body":199,"takeway":200,"prerequisite":201},142,"The answer: The Breakpoint Was on the Wrong Block","Jawabannya: Breakpoint-nya Ada di Blok yang Salah","breakpoint-on-the-wrong-block","Eight measured states of the same loop, from 0.0% to 90.9% cache hits: which fix moved the bill, which two moved nothing, and why the transcript got longer while the cost fell.","Delapan keadaan terukur dari loop yang sama, dari 0,0% ke 90,9% cache hit: perbaikan mana yang menggerakkan tagihan, dua perbaikan yang tidak mengubah apa pun, dan kenapa transkripnya jadi lebih panjang sementara biayanya turun.","2026-11-08T01:30:00.000Z",3,[70,83,84,85],"llm","cache","api","\u002Fog\u002Fbreakpoint-on-the-wrong-block.png",{"id":88,"title":69,"title_id":69,"slug":70,"description":89,"description_id":90,"articles":91,"children_series":192,"parent_series":21},29,"Practice labs: work the problem first, then read the answer. Every lab ships with a repo you can clone, production-sized data, and a target you have to hit yourself. The write-up lives in this series too, and it only opens from the problem article.","Lab praktik: kerjakan masalahnya dulu, baru lihat jawabannya. Tiap lab punya repo yang bisa di-clone, data seukuran produksi, dan target yang harus kamu capai sendiri. Artikel jawabannya ada di seri ini juga, dan cuma bisa kamu buka dari artikel masalahnya.",[92,113,131,146,163,178],{"id":93,"title":94,"title_id":95,"slug":96,"excerpt":97,"excerpt_id":98,"has_translation":12,"published_at":99,"reading_time":14,"hidden_from_listing":15,"tags":100,"thumbnail_image":21,"og_image":104,"series":105,"authors":106},104,"N+1 Was Never the Bottleneck: 41 Queries Down to 2, Still One Second","Query-nya Tinggal 2, Tapi Response-nya Masih 1 Detik","query-tinggal-2-tapi-response-masih-1-detik","A feed endpoint that shows 20 posts takes a full second and hits the database with 41 queries. Apply the standard N+1 advice, get it down to 2 queries, and the latency does not move. This is that problem as it happened, plus the lab.","Endpoint feed yang cuma menampilkan 20 tulisan butuh satu detik, dan server database dihujani 41 query. Kamu kerjakan nasihat N+1 yang standar, query-nya turun jadi 2, dan latency-nya nggak bergerak. Ini versi \"apa adanya\" dari masalah itu, plus lab-nya.","2026-10-04T09:59:21.000Z",[101,102,103],"database","performance","backend","\u002Fog\u002Fquery-tinggal-2-tapi-response-masih-1-detik.png",{"id":88,"title":69,"title_id":69,"slug":70},[107],{"authors_id":108},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},1,"Daffa Izzuddin","izzudd","Writer",{"id":114,"title":115,"title_id":116,"slug":117,"excerpt":118,"excerpt_id":119,"has_translation":12,"published_at":120,"reading_time":121,"hidden_from_listing":15,"tags":122,"thumbnail_image":21,"og_image":126,"series":127,"authors":128},114,"One Payment, Four Ledger Rows: An Idempotency Lab","Saldo Naik Empat Kali untuk Satu Pembayaran","saldo-naik-empat-kali-untuk-satu-pembayaran","The code is not wrong and the gateway is not wrong, yet one payment shows up four times in the ledger. Guess why, then make `make check` green in the lab.","Kodenya nggak salah, dan gateway-nya juga nggak salah. Tapi satu pembayaran muncul empat kali di ledger. Coba tebak kenapa — lalu bikin `make check` hijau di lab-nya.","2026-10-11T13:45:00.000Z",4,[123,124,125,101,70],"idempotency","webhook","postgres","\u002Fog\u002Fsaldo-naik-empat-kali-untuk-satu-pembayaran.png",{"id":88,"title":69,"title_id":69,"slug":70},[129],{"authors_id":130},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":132,"title":133,"title_id":134,"slug":135,"excerpt":136,"excerpt_id":137,"has_translation":12,"published_at":138,"reading_time":81,"hidden_from_listing":15,"tags":139,"thumbnail_image":21,"og_image":141,"series":142,"authors":143},116,"The Webhook Arrived Late. The Reconciliation Job Had Already Run.","Webhook-nya Telat, Job Rekonsiliasinya Keburu Jalan","webhook-telat-job-rekonsiliasi-keburu-jalan","The webhook path is already idempotent. Then a morning reconciliation job appears, and one payment gets recorded twice from two paths that are both right. Guess where the key is wrong.","Jalur webhook sudah idempotent. Lalu ada job rekonsiliasi pagi, dan satu pembayaran tercatat dua kali dari dua jalur yang dua-duanya benar. Coba tebak kuncinya salah di mana.","2026-10-11T13:44:00.000Z",[123,140,124,125,70],"reconciliation","\u002Fog\u002Fwebhook-telat-job-rekonsiliasi-keburu-jalan.png",{"id":88,"title":69,"title_id":69,"slug":70},[144],{"authors_id":145},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":147,"title":148,"title_id":149,"slug":150,"excerpt":151,"excerpt_id":152,"has_translation":12,"published_at":153,"reading_time":121,"hidden_from_listing":15,"tags":154,"thumbnail_image":21,"og_image":158,"series":159,"authors":160},120,"Every Receipt in This Café Lists the Same Order","Tiap Nota di Kafe Ini Isinya Pesanan yang Sama","tiap-nota-isinya-pesanan-yang-sama","Every table's receipt holds the same items, and the total is Rp0 — even though each table ordered exactly one thing. Guess why, then make `make check` green. A Python scope and mutation lab.","Nota tiap meja isinya sama semua, dan totalnya Rp0 — padahal tiap meja cuma pesan satu barang. Coba tebak kenapa.","2026-10-11T23:00:15.000Z",[155,156,157,70],"python","scope","mutasi","\u002Fog\u002Ftiap-nota-isinya-pesanan-yang-sama.png",{"id":88,"title":69,"title_id":69,"slug":70},[161],{"authors_id":162},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":164,"title":165,"title_id":166,"slug":167,"excerpt":168,"excerpt_id":169,"has_translation":12,"published_at":153,"reading_time":121,"hidden_from_listing":15,"tags":170,"thumbnail_image":21,"og_image":173,"series":174,"authors":175},122,"A Script That Cannot Say Where It Went Wrong","Script yang Nggak Bisa Bilang di Mana Salahnya","script-yang-tidak-bilang-di-mana-salahnya","A script that stops at the first bad line reports only part of the problem. This is what shows up on screen right now.","Script yang berhenti di baris jelek pertama cuma melaporkan sebagian masalah yang ada. Ini yang muncul di layar sekarang.",[155,171,172,70],"error-handling","validasi","\u002Fog\u002Fscript-yang-tidak-bilang-di-mana-salahnya.png",{"id":88,"title":69,"title_id":69,"slug":70},[176],{"authors_id":177},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":179,"title":180,"title_id":181,"slug":182,"excerpt":183,"excerpt_id":184,"has_translation":12,"published_at":185,"reading_time":81,"hidden_from_listing":15,"tags":186,"thumbnail_image":21,"og_image":187,"series":188,"authors":189},141,"A 6,000-Token Handbook and a 0% Cache Hit Rate","Buku Panduan 6.000 Token dan Hit Rate Cache 0%","cache-hit-rate-zero","Twenty-four turns, caching switched on, and a bill 21% higher than running with no cache at all. A support agent whose prompt looks static, and the three lines that quietly change it every turn.","Dua puluh empat giliran, caching menyala, dan tagihan 21% lebih tinggi daripada menjalankannya tanpa cache sama sekali. Agen support yang prompt-nya kelihatan statis, dan tiga baris yang diam-diam mengubahnya tiap giliran.","2026-11-08T01:20:00.000Z",[70,83,84,85],"\u002Fog\u002Fcache-hit-rate-zero.png",{"id":88,"title":69,"title_id":69,"slug":70},[190],{"authors_id":191},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},[],[194],{"authors_id":195},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},"en",[196,198],"id","\n**This is the answer article.** If you have not worked lab 6 and still want to try it yourself, close the page first. Everything below is the answer, with the measured numbers.\n\nThe interesting part is not the fix. It is that three of the five edits changed no number at all for two stages, and the one that did change everything is a single line nobody would flag in review.\n\n## Eight states, one loop\n\nMeasured with `scripts\u002Fcheck.py`, on the same machine, without touching `data\u002F`, `scripts\u002Fcheck.py` or `agent\u002Fprovider.py`:\n\n| stage | change | hit rate | bill | green |\n|---|---|---|---|---|\n| 0 | as shipped | 0.0% | $0.4841 | 0\u002F7 |\n| 1 | the clock leaves the system prompt | 0.0% | $0.4840 | 0\u002F7 |\n| 2 | + a breakpoint at the end of the handbook | 0.0% | $0.4840 | 0\u002F7 |\n| 3 | + a stable tools array | 78.4% | $0.1865 | 2\u002F7 |\n| 4 | + an append-only transcript | 80.4% | $0.1857 | 4\u002F7 |\n| 5 | + the date moved into the user turn | 80.4% | $0.1860 | 5\u002F7 |\n| 6 | + a one-hour TTL for the handbook | 90.9% | $0.1437 | 6\u002F7 |\n| 7 | + the loop reports its own numbers | 90.9% | $0.1437 | 7\u002F7 |\n\nThe same requests with caching switched off cost $0.4017 at stage 0 and $0.4218 at stage 7. The second number is bigger because the fixed loop carries its transcripts instead of summarising them.\n\n## The tools array, which is where the money was\n\n```python\ndef tools_for_request() -> list[dict]:\n    return TOOLS\n```\n\nStage 3 is one line, and it takes the hit rate from nothing to most of it. The provider hashes the tool definitions before it hashes the system prompt, so a `notes` field holding the registry snapshot time invalidates everything below it, on every request, forever. The model never read that field. It was there for a human.\n\nThe order matters too. Removing the timestamp from the system prompt at stage 1 was correct and bought nothing, because the tools were still handing the cache a different prefix each turn. A prefix is only as stable as its least stable part, and the tools sit at the top.\n\n## The breakpoint, and the date that has to move\n\nA breakpoint does two jobs: it writes an entry at that boundary, and it is the position the next request looks back from. Parking it on the last message means the entry is written at a boundary that the next turn extends, so the next request finds nothing unless the lookback window reaches an older write.\n\n```python\nblocks = [Block(role=\"system\", text=system_prompt(), cache=True, ttl=LONG_TTL)]\n```\n\nTwo breakpoints end up being the right shape: one at the end of the handbook, kept for an hour, and one at the end of the growing transcript, kept for the default five minutes. The first is what every ticket shares; the second is what saves the previous turn.\n\nThe date then has to move rather than disappear. Check C fails if the static prefix changes between two days, and fails again if the date never reaches the model, so the only fix that passes both is to stamp the turn:\n\n```python\nhistory.append({\"role\": \"user\", \"text\": f\"{text}\\n\\n[session date {today(provider.clock)}]\"})\n```\n\nStage 5 is where check C turns green and the bill does not move at all, which is the point. Correctness and cost are separate gates here.\n\n## The rewrite nobody counted\n\nEvery third turn the loop replaced the transcript with a one-line summary. It saved 10,029 tokens over the run and it made every prefix after the second one useless. The fixed version sends 174,883 tokens against the original 164,854 and pays $0.1437 against $0.4841.\n\nIf you need a summary, append it as a new message at the end. The earlier blocks stay untouched, the prefix still matches, and the summary becomes part of the cached history instead of a hole in it.\n\n## The lifetime is a bill multiplier, not a detail\n\nWith a five-minute default, the customer's ten-minute silence costs one full rewrite of 6,135 tokens per ticket. With the one-hour write at 1.25x on that block only, the pause costs nothing: the turn after it reads 6,135 tokens from cache. The write premium is paid once per hour per handbook, and the handbook does not change.\n\nNote the shape of that decision. The static block gets the expensive, long-lived write; the transcript gets the cheap, short-lived one. One TTL for the whole prompt would be wrong in both directions.\n\n## The counter is part of the fix\n\nCheck G is the last one for a reason. It runs the loop as a program and compares the line it prints with the ledger:\n\n```plain\n$ python3 agent\u002Floop.py\nsummary turns=24 hit_rate=0.909 cost_usd=0.143682\n```\n\nWithout that line, stage 0 and stage 7 are indistinguishable from the outside: both answer every ticket, both exit 0, both look fine. A number that the loop reports about itself and a checker re-derives is the difference between a fix and a belief.\n","- **The tools array was the single expensive line.** A registry timestamp inside a tool description kept the hit rate at 0.0%; making the definitions constant took it to 78.4%.\n- **Moving the clock and the breakpoint alone changed nothing.** Both were necessary, neither was sufficient, and the check output says so at every stage.\n- **The transcript rewrite was buying 10,029 tokens and costing four times the bill.** The fixed loop sends 6% more tokens and pays 34% of the no-cache run.\n- **The one-hour write is what survives a ten-minute pause.** After the pause, the turn that follows reads 6,135 tokens from cache instead of rewriting them.",{"id":179,"title":180,"slug":182},"\u003Cp>\u003Cstrong>This is the answer article.\u003C\u002Fstrong> If you have not worked lab 6 and still want to try it yourself, close the page first. Everything below is the answer, with the measured numbers.\u003C\u002Fp>\n\u003Cp>The interesting part is not the fix. It is that three of the five edits changed no number at all for two stages, and the one that did change everything is a single line nobody would flag in review.\u003C\u002Fp>\n\u003Ch2 id=\"eight-states-one-loop\">Eight states, one loop\u003C\u002Fh2>\n\u003Cp>Measured with \u003Ccode>scripts\u002Fcheck.py\u003C\u002Fcode>, on the same machine, without touching \u003Ccode>data\u002F\u003C\u002Fcode>, \u003Ccode>scripts\u002Fcheck.py\u003C\u002Fcode> or \u003Ccode>agent\u002Fprovider.py\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>stage\u003C\u002Fth>\n\u003Cth>change\u003C\u002Fth>\n\u003Cth>hit rate\u003C\u002Fth>\n\u003Cth>bill\u003C\u002Fth>\n\u003Cth>green\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>0\u003C\u002Ftd>\n\u003Ctd>as shipped\u003C\u002Ftd>\n\u003Ctd>0.0%\u003C\u002Ftd>\n\u003Ctd>$0.4841\u003C\u002Ftd>\n\u003Ctd>0\u002F7\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>1\u003C\u002Ftd>\n\u003Ctd>the clock leaves the system prompt\u003C\u002Ftd>\n\u003Ctd>0.0%\u003C\u002Ftd>\n\u003Ctd>$0.4840\u003C\u002Ftd>\n\u003Ctd>0\u002F7\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>+ a breakpoint at the end of the handbook\u003C\u002Ftd>\n\u003Ctd>0.0%\u003C\u002Ftd>\n\u003Ctd>$0.4840\u003C\u002Ftd>\n\u003Ctd>0\u002F7\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>3\u003C\u002Ftd>\n\u003Ctd>+ a stable tools array\u003C\u002Ftd>\n\u003Ctd>78.4%\u003C\u002Ftd>\n\u003Ctd>$0.1865\u003C\u002Ftd>\n\u003Ctd>2\u002F7\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>4\u003C\u002Ftd>\n\u003Ctd>+ an append-only transcript\u003C\u002Ftd>\n\u003Ctd>80.4%\u003C\u002Ftd>\n\u003Ctd>$0.1857\u003C\u002Ftd>\n\u003Ctd>4\u002F7\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>5\u003C\u002Ftd>\n\u003Ctd>+ the date moved into the user turn\u003C\u002Ftd>\n\u003Ctd>80.4%\u003C\u002Ftd>\n\u003Ctd>$0.1860\u003C\u002Ftd>\n\u003Ctd>5\u002F7\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>6\u003C\u002Ftd>\n\u003Ctd>+ a one-hour TTL for the handbook\u003C\u002Ftd>\n\u003Ctd>90.9%\u003C\u002Ftd>\n\u003Ctd>$0.1437\u003C\u002Ftd>\n\u003Ctd>6\u002F7\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>7\u003C\u002Ftd>\n\u003Ctd>+ the loop reports its own numbers\u003C\u002Ftd>\n\u003Ctd>90.9%\u003C\u002Ftd>\n\u003Ctd>$0.1437\u003C\u002Ftd>\n\u003Ctd>7\u002F7\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>The same requests with caching switched off cost $0.4017 at stage 0 and $0.4218 at stage 7. The second number is bigger because the fixed loop carries its transcripts instead of summarising them.\u003C\u002Fp>\n\u003Ch2 id=\"the-tools-array-which-is-where-the-money-was\">The tools array, which is where the money was\u003C\u002Fh2>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"def%20tools_for_request()%20-%3E%20list%5Bdict%5D%3A%0A%20%20%20%20return%20TOOLS\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> tools_for_request\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">() -> list[\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">dict\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">]:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> TOOLS\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Stage 3 is one line, and it takes the hit rate from nothing to most of it. The provider hashes the tool definitions before it hashes the system prompt, so a \u003Ccode>notes\u003C\u002Fcode> field holding the registry snapshot time invalidates everything below it, on every request, forever. The model never read that field. It was there for a human.\u003C\u002Fp>\n\u003Cp>The order matters too. Removing the timestamp from the system prompt at stage 1 was correct and bought nothing, because the tools were still handing the cache a different prefix each turn. A prefix is only as stable as its least stable part, and the tools sit at the top.\u003C\u002Fp>\n\u003Ch2 id=\"the-breakpoint-and-the-date-that-has-to-move\">The breakpoint, and the date that has to move\u003C\u002Fh2>\n\u003Cp>A breakpoint does two jobs: it writes an entry at that boundary, and it is the position the next request looks back from. Parking it on the last message means the entry is written at a boundary that the next turn extends, so the next request finds nothing unless the lookback window reaches an older write.\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"blocks%20%3D%20%5BBlock(role%3D%22system%22%2C%20text%3Dsystem_prompt()%2C%20cache%3DTrue%2C%20ttl%3DLONG_TTL)%5D\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">blocks \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [Block(\u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">role\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"system\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">text\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">system_prompt(), \u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">cache\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">True\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">ttl\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">LONG_TTL\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)]\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Two breakpoints end up being the right shape: one at the end of the handbook, kept for an hour, and one at the end of the growing transcript, kept for the default five minutes. The first is what every ticket shares; the second is what saves the previous turn.\u003C\u002Fp>\n\u003Cp>The date then has to move rather than disappear. Check C fails if the static prefix changes between two days, and fails again if the date never reaches the model, so the only fix that passes both is to stamp the turn:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"history.append(%7B%22role%22%3A%20%22user%22%2C%20%22text%22%3A%20f%22%7Btext%7D%5Cn%5Cn%5Bsession%20date%20%7Btoday(provider.clock)%7D%5D%22%7D)\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">history.append({\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"role\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"user\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"text\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">text\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\\n\\n\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">[session date \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">today(provider.clock)\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">]\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">})\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Stage 5 is where check C turns green and the bill does not move at all, which is the point. Correctness and cost are separate gates here.\u003C\u002Fp>\n\u003Ch2 id=\"the-rewrite-nobody-counted\">The rewrite nobody counted\u003C\u002Fh2>\n\u003Cp>Every third turn the loop replaced the transcript with a one-line summary. It saved 10,029 tokens over the run and it made every prefix after the second one useless. The fixed version sends 174,883 tokens against the original 164,854 and pays $0.1437 against $0.4841.\u003C\u002Fp>\n\u003Cp>If you need a summary, append it as a new message at the end. The earlier blocks stay untouched, the prefix still matches, and the summary becomes part of the cached history instead of a hole in it.\u003C\u002Fp>\n\u003Ch2 id=\"the-lifetime-is-a-bill-multiplier-not-a-detail\">The lifetime is a bill multiplier, not a detail\u003C\u002Fh2>\n\u003Cp>With a five-minute default, the customer&#39;s ten-minute silence costs one full rewrite of 6,135 tokens per ticket. With the one-hour write at 1.25x on that block only, the pause costs nothing: the turn after it reads 6,135 tokens from cache. The write premium is paid once per hour per handbook, and the handbook does not change.\u003C\u002Fp>\n\u003Cp>Note the shape of that decision. The static block gets the expensive, long-lived write; the transcript gets the cheap, short-lived one. One TTL for the whole prompt would be wrong in both directions.\u003C\u002Fp>\n\u003Ch2 id=\"the-counter-is-part-of-the-fix\">The counter is part of the fix\u003C\u002Fh2>\n\u003Cp>Check G is the last one for a reason. It runs the loop as a program and compares the line it prints with the ledger:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper code-output my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden text-[#e1e4e8] transition-all\" data-output-block>\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"code-output-mark\" aria-hidden=\"true\">&gt;_\u003C\u002Fspan>\n      \u003Cspan class=\"ml-1 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">Output\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%24%20python3%20agent%2Floop.py%0Asummary%20turns%3D24%20hit_rate%3D0.909%20cost_usd%3D0.143682\" title=\"Copy\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\u003Ccode>\u003Cspan class=\"code-output-cmd\">$ python3 agent\u002Floop.py\u003C\u002Fspan>\nsummary turns=24 hit_rate=0.909 cost_usd=0.143682\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Without that line, stage 0 and stage 7 are indistinguishable from the outside: both answer every ticket, both exit 0, both look fine. A number that the loop reports about itself and a checker re-derives is the difference between a fix and a belief.\u003C\u002Fp>\n","\u003Cul>\n\u003Cli>\u003Cstrong>The tools array was the single expensive line.\u003C\u002Fstrong> A registry timestamp inside a tool description kept the hit rate at 0.0%; making the definitions constant took it to 78.4%.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Moving the clock and the breakpoint alone changed nothing.\u003C\u002Fstrong> Both were necessary, neither was sufficient, and the check output says so at every stage.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The transcript rewrite was buying 10,029 tokens and costing four times the bill.\u003C\u002Fstrong> The fixed loop sends 6% more tokens and pays 34% of the no-cache run.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The one-hour write is what survives a ten-minute pause.\u003C\u002Fstrong> After the pause, the turn that follows reads 6,135 tokens from cache instead of rewriting them.\u003C\u002Fli>\n\u003C\u002Ful>\n",[205,227,248,266,271,288],{"id":206,"title":207,"title_id":208,"slug":209,"excerpt":210,"excerpt_id":211,"has_translation":12,"published_at":212,"reading_time":121,"hidden_from_listing":15,"tags":213,"thumbnail_image":21,"og_image":218,"series":219,"authors":224},48,"What a Linked List Actually Is","Tentang Struktur Data LinkedList","tentang-struktur-data-linked-list","A linked list is a linear chain of nodes, each holding data and a pointer to the next node. Here is what that chain looks like, what it costs next to an array, and the same minimal implementation in Java, Python, and JavaScript.","Linked list adalah rantai linear berisi node, dan setiap node menyimpan data plus pointer ke node berikutnya. Ini bentuk rantainya, ongkosnya dibanding array, dan implementasi paling sederhana dalam Java, Python, serta JavaScript.","2026-11-21T01:00:00.000Z",[214,215,216,217],"struktur-data","linked-list","pointer","konsep","\u002Fog\u002Ftentang-struktur-data-linked-list.png",{"id":220,"title":221,"title_id":222,"slug":223},28,"Linear Data Structures","Struktur Data Linear","asd-struktur-linear",[225],{"authors_id":226},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":228,"title":229,"title_id":230,"slug":231,"excerpt":232,"excerpt_id":233,"has_translation":12,"published_at":234,"reading_time":235,"hidden_from_listing":15,"tags":236,"thumbnail_image":21,"og_image":239,"series":240,"authors":245},47,"Why Understanding Algorithms and Data Structures Actually Matters","Pentingnya Memahami Algoritma dan Struktur Data","pentingnya-memahami-algoritma-dan-struktur-data","An algorithm is an ordered list of steps, and that order decides how expensive a problem is. The examples are on the back of an instant noodle packet, in a factorial, and behind ctrl+z.","Sebagai konsep fundamental, algoritma menentukan seberapa mahal sebuah masalah diselesaikan. Contohnya ada di bungkus mie instan, di bilangan faktorial, dan di tombol undo.","2026-11-19T01:00:00.000Z",7,[237,214,217,238],"algoritma","dasar","\u002Fog\u002Fpentingnya-memahami-algoritma-dan-struktur-data.png",{"id":241,"title":242,"title_id":243,"slug":244},27,"About Algorithms and Data Structures","Tentang Algoritma dan Struktur Data","asd-intro",[246],{"authors_id":247},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":249,"title":250,"title_id":251,"slug":252,"excerpt":253,"excerpt_id":254,"has_translation":12,"published_at":255,"reading_time":14,"hidden_from_listing":15,"tags":256,"thumbnail_image":21,"og_image":261,"series":262,"authors":263},108,"At 11 a.m. the AI Quota Runs Out, and the Work Stops There","Jam 11 siang kuota AI-nya habis, dan kerjaan berhenti di situ","jam-11-kuota-ai-habis-kerjaan-berhenti","The panel says the usage limit is reached and the work stops, not because it is finished. Three guesses explain it, and all three are wrong: the limit is shaped like a cliff, and the day is built on top of one tool.","Panel di layar bilang batas pemakaian tercapai, dan kerja berhenti di situ. Bukan karena selesai. Ada tiga tebakan kenapa ini terjadi, dan ketiganya salah: batasnya berbentuk tebing, dan harinya dibangun di atas satu alat.","2026-11-12T01:00:00.000Z",[257,258,259,260],"ai","tooling","rate-limit","produktivitas","\u002Fog\u002Fjam-11-kuota-ai-habis-kerjaan-berhenti.png",{"id":24,"title":25,"title_id":26,"slug":27},[264],{"authors_id":265},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":179,"title":180,"title_id":181,"slug":182,"excerpt":183,"excerpt_id":184,"has_translation":12,"published_at":185,"reading_time":81,"hidden_from_listing":15,"tags":186,"thumbnail_image":21,"og_image":187,"series":267,"authors":268},{"id":88,"title":69,"title_id":69,"slug":70},[269],{"authors_id":270},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":272,"title":273,"title_id":274,"slug":275,"excerpt":276,"excerpt_id":277,"has_translation":12,"published_at":278,"reading_time":279,"hidden_from_listing":15,"tags":280,"thumbnail_image":21,"og_image":282,"series":283,"authors":285},140,"What a Prompt Cache Actually Remembers","Apa yang Sebenarnya Diingat Prompt Cache","what-a-prompt-cache-remembers","Four providers cache the same bytes under four different rulebooks. The question that decides your bill: when the second request is longer than the first, does it reuse the whole prefix or only the part that was already written?","Empat provider meng-cache byte yang sama dengan empat buku aturan berbeda. Pertanyaan yang menentukan tagihanmu: saat permintaan kedua lebih panjang, apakah seluruh prefix dipakai ulang atau cuma bagian yang sudah pernah ditulis?","2026-11-08T01:10:00.000Z",6,[83,281,84,85],"ai-agent","\u002Fog\u002Fwhat-a-prompt-cache-remembers.png",{"id":284,"title":35,"title_id":36,"slug":37},8,[286],{"authors_id":287},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},{"id":289,"title":290,"title_id":291,"slug":292,"excerpt":293,"excerpt_id":294,"has_translation":12,"published_at":295,"reading_time":14,"hidden_from_listing":15,"tags":296,"thumbnail_image":21,"og_image":299,"series":300,"authors":305},138,"Python venv: the Install That Went Into the Wrong Python","venv Python: Instalasi yang Masuk ke Python yang Salah","python-venv-pip","`pip install` printed Successfully installed, and the next run raised ModuleNotFoundError on the same machine. Two interpreters, one install, and the variable your script checks is the one the interpreter never sets.","`pip install` mencetak Successfully installed, lalu jalan berikutnya melempar ModuleNotFoundError di mesin yang sama. Dua interpreter, satu instalasi, dan variabel yang skripmu periksa justru tidak pernah dipasang oleh interpreter.","2026-11-07T12:00:00.000Z",[155,297,298],"venv","pip","\u002Fog\u002Fpython-venv-pip.png",{"id":301,"title":302,"title_id":303,"slug":304},18,"Working with Python Scripts","Menggunakan Script Python","python-pemrograman-dengan-script",[306],{"authors_id":307},{"id":109,"name":110,"slug":111,"role":112,"profile_picture":21},[309,313,316,319,322,325],{"text":310,"depth":311,"id":312},"Eight states, one loop",2,"eight-states-one-loop",{"text":314,"depth":311,"id":315},"The tools array, which is where the money was","the-tools-array-which-is-where-the-money-was",{"text":317,"depth":311,"id":318},"The breakpoint, and the date that has to move","the-breakpoint-and-the-date-that-has-to-move",{"text":320,"depth":311,"id":321},"The rewrite nobody counted","the-rewrite-nobody-counted",{"text":323,"depth":311,"id":324},"The lifetime is a bill multiplier, not a detail","the-lifetime-is-a-bill-multiplier-not-a-detail",{"text":326,"depth":311,"id":327},"The counter is part of the fix","the-counter-is-part-of-the-fix",1794150713352]