开发者
개발일지 자동화가 한 달 가까이 멈춰 있었던 이유
로그조차 안 쌓이니, 돌고 있는지 죽어 있는지 구분할 방법이 없었습니다 이 블로그의 개발일지는 매일 밤 자동으로 마무리됩니다. 그날 대화로 초안을 썼으면 변환해서 로그에 남기고, 없으면 스킵했다는 한 줄만 남깁니다. 최근 이 파이프라인을 들여다볼 일이 있었는데, 7월 24일 이후로 로그가 통째로 비어 있었습니다. 무슨 일이 있었나 자동화 로그( .automation.log ) 마지막 줄이 2026-07-24였습니다. 그 뒤로 8월 22일까지, 거의 한 달 가까이 스킵 기록조차 한 줄도 없었습니다. 자동화가 아예 안 돌았나 싶어서 실행 로그( cron_output.log )를 열어봤습니다. 그런데 거기엔 매일 밤 실행된 흔적이 빼곡했습니다. 날짜 확인하고, 초안 있으면 내용 정리하고, 크로스링크까지 챙긴 요약이 매일 밤 남아 있었습니다. 일은 하고 있었는데, 결과물만 하나도 남지 않고 있었던 겁니다. 왜 아무도 몰랐나 실행 로그를 읽어보니 원인은 매일 같았습니다. 파일 쓰기 권한 승인을 기다리다가 그대로 끝난 겁니다. "workspace has not been trusted"라는 경고가 매 실행마다 찍혀 있었습니다. 초안을 잘 정리해놓고도, 마지막 파일 쓰기 한 줄이 승인 대기에 막혀서 아무것도 저장되지 않은 채 세션이 끝나는 패턴이 한 달 가까이 반복됐습니다. 문제는 이게 하필 로그를 남기는 단계 자체가 막힌 상황 이었다는 겁니다. 스킵한 날엔 스킵했다는 한 줄도 못 남겼습니다. 그러니 로그만 보면 "자동화가 멈췄다"와 "쓸 게 없어서 조용했다"를 구분할 수가 없었습니다. 무인 자동화이니 매일 밤 누가 화면을 지켜보는 것도 아닙니다. 결과적으로 이 공백은 사람이 우연히 로그 파일을 열어보기 전까지는 발견될 방법이 없는 구조였습니다. 진짜 원인 원인은 신뢰(trust) 설정이었습니다. 이 헤드리스 자동화 세션은 프로젝트 폴더 단위로 파일 쓰기 권한을 신뢰받아야 동작하는데, 그 신뢰 설정 키가 이 블로그 폴더가 아니라 상위 디렉터리(프로젝트들이 모여 있는 루트) 단위로 걸려 있었습니다. 즉 이 블로그 폴더만 놓고 보면 "아직 한 번도 대화형으로 신뢰 승인을 받은 적 없는 새 작업공간" 취급을 받고 있었던 셈입니다. 설정 파일 안에 이미 허용 규칙 10개가 들어 있었는데도, 그 규칙들이 걸려 있는 범위 자체가 무시되고 있었습니다. 헤드리스로 도는 야간 자동화는 대화형 승인 프롬프트에 응답할 사람이 없습니다. 범위가 어긋난 신뢰 설정 하나가, 매일 밤 정확히 같은 지점에서 조용히 실행을 무력화하고 있었던 겁니다. 흥미로운 디테일 하나 — 유령 완료 기록 이 공백을 되짚어보다가 특이한 줄 하나를 발견했습니다. 8월 22일 낮 12시 41분에 "weekly: 완료"라는 로그 한 줄이 남아 있었는데, 그 시각에 대응하는 실제 산출물 파일은 없었습니다. 같은 날 오후 4시에 다시 수동으로 실행된 기록이 있었고, 이번엔 실제 파일까지 정상적으로 만들어졌습니다. 앞선 12시 41분 기록이 왜 실물 없이 "완료"라고만 남았는지는 원인을 특정하지 못했습니다. 권한 문제가 한창이던 구간이라 어떤 형태로든 쓰기 절차 일부만 성공하고 일부는 실패한 걸로 추정만 할 뿐입니다. 다만 이 한 줄은 별도로 눈에 띄는 교훈을 남겼습니다. "완료"라고 적힌 로그도 그 자체로 완전히 믿을 수는 없다 는 겁니다. 로그와 실제 산출물을 따로 대조하지 않았다면 이 유령 기록을 그냥 지나쳤을 겁니다. 어떻게 고쳤나 신뢰 설정을 이 블로그 폴더 기준으로 다시 걸어주니, 그날 밤부터 바로 정상화됐습니다. 별도의 복잡한 조치는 필요 없었습니다. 문제는 고치는 방법이 아니라, 한 달 가까이 그 문제를 놓치고 있었다는 사실 쪽이었습니다. 일반화된 교훈 이번 일로 다시 확인한 건, 무인 자동화에서 "로그가 없다"는 상태 자체가 하나의 신호라는 겁니다. 그런데 그 신호를 신호로 취급하려면, 애초에 "침묵"과 "성공적인 무동작"을 구분할 수 있게 설계돼 있어야 합니다. 이번 파이프라인은 스킵한 날에도 로그 한 줄을 남기게 되어 있었습니다. 그 설계 덕분에, "로그가 아예 안
AI 资讯
A Reason Code Without a Source Is Half a Diagnostic
A failure message can be technically correct and still be frustratingly incomplete. Consider a timeout. It tells us something important about the failure mechanism, but not which operation encountered it. Adding the complete request target might answer that question, yet it can also expose identifiers, query parameters, access material, or other data that never belonged in a broadly visible diagnostic record. A safer middle ground is to give failures two separate coordinates: a reason code that explains how the operation failed, and a bounded operation label that explains where it failed. That distinction makes diagnostics more useful without turning failure handling into an accidental data-exposure channel. A reason code is not a location Reason codes describe failure mechanics. Generic examples might include deadline , cancelled , unauthorised , or invalid_response . These codes are valuable because they let systems group similar outcomes. A dashboard can count deadline failures across operations, while application logic can decide whether a particular reason is retryable. What a reason code cannot reliably explain is the operation being attempted. A deadline during a summary read may require a different investigation from a deadline while assembling a detailed response. Combining both meanings into one free-form message makes failures harder to query and encourages presentation text to become an informal data model. Model the two coordinates separately A deliberately generic, invented C# model might look like this: public enum OperationArea { Summary , Detail , Archive } public sealed record FailureDetail ( string ReasonCode , OperationArea ? Area = null ); The reason remains suitable for classification. The operation label adds location without carrying an unrestricted request value. An enum is not the only option. A validated value object or centrally managed set of constants can work too. The important constraint is that labels come from a small, reviewed voca
AI 资讯
My probe passed because it could not fail
Originally published on hexisteme notes . I run pre-registered checks against a live system, read the verdict, and move on — that's the whole point of pre-registering them, so I don't get to argue with the result after the fact. Most of the time the discipline pays for itself. This time it passed, and the pass was wrong, and the reason it was wrong is more interesting than the failure itself: the check could not have returned anything else, whatever had actually happened to the file under test. The question I was probing something narrow: does a hand-made audio crossfade survive a round trip through DaVinci Resolve? Build a timeline with a crossfade sitting on a cut, export it to FCPXML 1.10, re-import it, and see whether the crossfade is still there. Third-party documentation says transitions are invisible to and unmodifiable by the scripting API. Believing that, I pre-registered a judgment method that never looks at timeline structure at all: render audio around the splice and classify it by waveform shape. The judge, exactly as pre-registered: render two seconds either side of the cut, downsample to 8 kHz mono, compute a 20 ms sliding-window RMS envelope — 202 windows across the render — and take the largest normalized step between adjacent windows. Above 0.5, call it a hard cut: the fade is gone. Below 0.5, call it a gradual ramp: the fade survived. The probe came back pass — gradual ramp, max step 0.4761, under the 0.5 threshold. Exit 0, all green. The crossfade had actually been lost at the export step. The pass was a false confirm, and I only found that out by going back in with a second, read-only inspection after the fact. Why the check could not fail The prep instructions for this probe — which I also wrote — said the easiest way to get two adjacent audio items with enough handle to build a crossfade is to take one continuous clip and blade-split it in the middle. That's a completely reasonable instruction on its own. A crossfade needs overlap media on bot
AI 资讯
Fix Next.js "params should be awaited" Error in Next.js 15+
Fix Next.js "params should be awaited" Error in Next.js 15+ If you are seeing the params should be awaited Next.js error after upgrading to Next.js 15 or following an older App Router tutorial, you are not alone. The error usually looks something like this: Route "/blog/[slug]" used params.slug. params should be awaited before using its properties. Sometimes it appears with searchParams . Sometimes it appears with cookies() or headers() . And sometimes the page still seems to work, but your terminal keeps shouting at you. This article will slow it down and explain the fix in a beginner-friendly way. No deep framework lecture first. Just the actual problem, the broken code, the fixed code, and the reason it works. What This Error Means in Plain English In older Next.js code, you may have treated params like a normal JavaScript object. Something like this: const slug = params . slug ; That used to feel natural. If your route was: /blog/[slug] and the user opened: /blog/my-first-post you expected: params . slug ; // "my-first-post" In newer Next.js versions, especially Next.js 15+, some request-based values became asynchronous. That means you should treat them like values that need to be waited for before you read from them. So instead of reading params.slug directly, you do this: const { slug } = await params ; That is the heart of the fix. The error is not saying your route is missing. It is not saying your [slug] folder is wrong. It is saying: You are trying to read route data before awaiting it. The common flow: the page loads, the code reads params.slug directly, Next.js expects params to be awaited, and the error appears. Why This Changed Next.js has a group of features called Dynamic APIs . That sounds more complicated than it is. In simple terms, Dynamic APIs are values that depend on the current request. For example: What route did the user open? What query string is in the URL? What cookies came with this request? What headers came with this request? Is draft
AI 资讯
A Dead PID Held My Lock for 2 Hours: One Missing Line, Zero Output, exit 0 Every Time
For 30 straight days as a college student earning ¥100k/month, I posted to Instagram by hand, and then I burned out and stopped. Today the same job runs on a Claude Code autonomous environment, I touch nothing, and it holds up ¥1.2M/month in revenue. Except for the two hours when it quietly stopped: three consecutive launchd runs, zero pieces of content generated, last exit=0 every single time, and not one alert. The cause was a process that had already been killed, holding a lock file nobody would take away from it. Why this setup works From "doing the work" to "building the environment" The problem with updating social media by hand is that it burns willpower. No matter how motivated you are, sleep, health, and mood all fluctuate. During the period when I was laid off and my income went to zero, I had no mental slack for posting at all. The autonomous environment I spent six months building with Claude Code runs regardless of my emotional state. launchd calls a script, the script generates content with claude -p (MAX plan quota; paid APIs are off-limits), the output is queued for auto-posting, and it goes out to Instagram every day at 19:30. As long as this machinery keeps working, ¥1.2M/month in sales holds up without me lifting a finger. The mental model I want to hand you A lot of people think "automation = writing scripts," and that's only half right. A script is correct at the moment you write it. Given time, external dependencies break, processes die for reasons you didn't anticipate, and lock files turn into debris that blocks every future run. An autonomous environment that actually works is one that assumes breakage and carries a layer that repairs it. The lock story here is a textbook case. ~/dev/brand-404/sns/gen_feature.py is a script launched on a schedule by launchd that auto-generates Instagram feature articles. A single run takes a long time (up to three claude -p calls, plus image generation, adding up to tens of minutes), so it has a lock mechani
AI 资讯
Fixing "g++ Not Found" When Debugging Rails in RubyMine on Fedora
Fixing "g++ Not Found" When Debugging Rails in RubyMine on Fedora TL;DR: If clicking "Debug" in RubyMine on Fedora fails to install debase because of a missing g++ compiler, the standard @development-tools group might not be enough. Run sudo dnf install gcc-c++ make redhat-rpm-config to get the explicit C++ compiler and tools Ruby needs to build native extensions. I was recently working on a Rails app on my Fedora machine and wanted to step through some code. I fired up RubyMine, set my breakpoints, and clicked the "Debug" button, fully expecting everything to just work. Instead, RubyMine tried to automatically install the debase and ruby-debug-ide gems—which it needs under the hood to hook into the Ruby process—and threw a massive wall of error text at me. The core of the failure looked like this: Building native extensions. This could take a while ... ERROR: Error installing debase-3.0.17.gem: ERROR: Failed to build gem native extension. ... /path/to/extconf_common.rb:80:in 'Kernel#`' : No such file or directory - g++ ( Errno::ENOENT ) My system was essentially complaining that it couldn't find g++ , the C++ compiler. The Initial (Failed) Attempt My first thought was, "Oh, I must have forgotten to install the base build tools on this machine." Since I'm on Fedora, I reached for the standard DNF command to pull in the development group: sudo dnf install @development-tools (Note: If you're on older versions of Fedora, you might be used to dnf groupinstall "Development Tools" , but DNF5 uses the @ syntax or space-separated group install ). It downloaded and installed a bunch of packages. I felt confident, went back to RubyMine, clicked "Debug" again, and... got the exact same No such file or directory - g++ error. Why Didn't That Work? When you install Ruby gems that contain native C or C++ extensions (like debase ), Ruby doesn't just download a pre-built binary. It actually compiles the raw source code down to machine code directly on your machine so it runs as fast
开发者
The Fix Was Committed. The Old Value Kept Running.
Originally published on hexisteme notes . I deleted three ambient API keys from my shell profile. Then I ran the standard clean-room check — spawn a shell with no inherited environment at all, env -i HOME="$HOME" /bin/zsh -lc 'echo "${VARNAME:-unset}"' , and read unset back for all three. That command doesn't lie: a shell started with an empty environment can only see what the current profile puts there, so if it reports the variable missing, the profile is clean. I closed the loop, reconnected my tools, and moved on. Minutes later I reconnected a review tool I run for cross-vendor sanity checks, and it came back healthy — with eight providers registered, one of them authenticated with a key I had just deleted. Not a cached credential from an old response. A live, working authentication, using a value that no longer existed anywhere on disk. The fix was committed. The old value kept running. Two different questions that sound like one "Did I fix the config?" and "Is the fix in effect?" collapse into a single question in your head, because in the common case they're the same event: you edit a file, the next thing that reads the file gets the new value, done. env -i answers the first question perfectly. It says nothing about the second, because it doesn't test any process that already exists — it only tests a brand-new one, freshly spawned, that has no choice but to read the current profile because it has no environment of its own yet. Every process that was already running before you made the edit is a different story. It read the profile once, at its own startup, copied whatever it found into its own memory, and has not looked at the file since. From that point forward it is not a reader of your shell profile — it is a cache of it. And caches don't invalidate themselves. Finding the actual culprit The process holding the stale value here was the editor I was working in — the same long-lived process that hosts my coding sessions and manages tool connections through M
AI 资讯
My Frontmatter Parser Checks for Too Few Delimiters. It Never Checked for Too Many.
I fixed this script's frontmatter parser a week ago. A draft with an unclosed --- block used to blow up with a bare ValueError: not enough values to unpack , and I patched it to raise a clean, actionable error instead. I wrote that fix up, verified it with a stubbed repro, added a --selftest case for it, called it done. Then I went back to write today's articles and actually looked at the line I "fixed" instead of the error path around it. def parse ( text ): meta = {} body = text if text . lstrip (). startswith ( " --- " ): parts = text . lstrip (). split ( " --- " , 2 ) if len ( parts ) < 3 : raise ValueError ( " frontmatter opened with ' --- ' but never closed with a second ' --- ' delimiter " ) _ , fm , body = parts ... split("---", 2) doesn't split on lines that are --- . It splits on the literal substring "---" , anywhere in the text, and stops after the second one it finds. My fix only handles the case where it finds fewer than two — an unclosed fence. It says nothing about what happens when the second "---" it finds isn't the closing fence at all, because a third one showed up first, buried inside a frontmatter value. That's not a hypothetical. I write these article titles myself, and "before/after" is a phrase I reach for constantly: --- title : My Before---After Refactor tags : ai, python, refactor published : true --- real body starts here split("---", 2) finds the em-dash-style --- inside the title before it finds the real closing fence on its own line. So the split points land in the wrong place entirely: >>> from publish_devto import parse >>> meta , body = parse ( text ) >>> meta { ' title ' : ' My Before ' } >>> body ' After Refactor \n tags: ai, python, refactor \n published: true \n --- \n real body starts here \n ' The title got truncated to "My Before" . tags and published never got parsed as frontmatter fields at all — they're sitting in the body now, as literal text, along with the real closing fence and a stray leftover --- . If I ran this thr
AI 资讯
The Case of the Vanishing Clipboard: Debugging a VirtualBox Guest Additions Conflict on Kali Linux
If you've ever run a Linux VM in VirtualBox and had copy-paste between your host and guest just... stop working, this post is for you. What started as a simple "my clipboard isn't syncing" turned into a proper detective story involving conflicting installations, a kernel module stuck "in use," and a systemd service quietly failing on every single boot. Here's the full walkthrough — what broke, how we figured out why, and how we fixed it for good. The Setup I run a Kali Linux VM inside VirtualBox on my host machine, mainly as a home lab for practicing infrastructure and security tooling. One day, shared clipboard between my host and the guest just stopped working. My first instinct was to run apt update && apt upgrade — but nothing changed. That's actually an important clue we'll come back to: apt upgrades regular packages, but it does not automatically rebuild or reinstall VirtualBox Guest Additions , which is the component actually responsible for clipboard sharing. What Actually Makes Clipboard Sharing Work Before diving into the fix, it helps to understand the moving parts, since "clipboard sync" isn't one single thing — it's three things working together: The vboxguest kernel module — a driver inside the guest OS that lets it talk to VirtualBox itself. VBoxService — a background daemon (runs as root) that handles ongoing communication with the hypervisor: time sync, clipboard, shared folders, and more. VBoxClient — a per-user process that specifically handles the clipboard and display integration, and talks to VBoxService through the kernel module. If any one of these three breaks, clipboard sharing breaks — and the error messages don't always make it obvious which one is the culprit. First Round: The Standard Checklist We started with the usual suspects for VirtualBox clipboard issues: Enable Bidirectional clipboard : In the VM window, under Devices > Shared Clipboard , this needs to be set to Bidirectional (or the direction you want). It resets sometimes after
AI 资讯
My MCP Tool's Empty-Payload Guard Checks Whether You Passed a Field. It Never Checked Whether the Field Would Actually Change Anything.
Back in early August I fixed a bug in update_article , one of the tools in this repo's DEV.to MCP server. The bug was straightforward: the tool built its PUT payload from three optional parameters, and if a caller passed none of them, it still fired a GET and a PUT with an empty {"article": {}} body against a live published post, then logged a no-op entry to the audit trail as if something had happened. The fix was a guard: raise before either network call if the built payload dict ends up empty. article = {} if title is not None : article [ " title " ] = title if body_markdown is not None : article [ " body_markdown " ] = body_markdown if published is not None : article [ " published " ] = published if not article : raise ValueError ( " update_article called with no fields to update " " (title/body_markdown/published all None) " ) before = _dev ( f " /articles/ { article_id } " ) result = _dev ( f " /articles/ { article_id } " , method = " PUT " , data = { " article " : article }) _log_article_update ( article_id , before , article . keys (), result ) I closed the ticket, ran a stubbed selftest, moved on. Going back into this function for something unrelated, I noticed the guard only ever asks one question: did the caller pass a field? It never asks the question that actually matters for a tool whose whole job is writing to a live post: would this field's value be different from what's already there? Walk through what happens if a caller — an agent that re-reads an article's current title before deciding whether to touch it, gets it slightly wrong, or just calls the tool defensively with the value it already has — passes title="Same Title It Already Has" , and that string is in fact identical to the article's current title. article isn't empty. It has one key. The guard passes clean. Both network calls fire: before = _dev ( f " /articles/ { article_id } " ) # GET, real call result = _dev ( f " /articles/ { article_id } " , method = " PUT " , data = { " article " :
AI 资讯
How to Fix 'NoneType' Object Has No Attribute Errors (Without Guessing)
Your script crashes, and near the bottom of the traceback sits AttributeError: 'NoneType' object has no attribute 'name' . It reads like Python is being deliberately unhelpful — but it's actually telling you something precise. You just tried to use a variable that turned out to be None , and it's telling you exactly which one and where. The error isn't saying your program is fundamentally broken. It's saying: at this exact line, you reached for an attribute on a value that was None instead of the object you expected. That's a narrow claim, and once you know how to read it, tracking down why it was None is usually mechanical. What the error is actually telling you Take this code: class User : def __init__ ( self , id , name ): self . id = id self . name = name def find_user ( users , user_id ): for u in users : if u . id == user_id : return u return None user = find_user ( users , target_id ) print ( user . name ) # AttributeError: 'NoneType' object has no attribute 'name' Read the message in two parts. 'NoneType' object has no attribute 'name' tells you the object you called .name on wasn't a User — it was None . has no attribute 'name' tells you which access failed. Put together: whatever user was pointing to when you hit that line wasn't what you expected — it was nothing at all. The message never claims .name is the problem. .name is just where the crash became visible. The real question is one step earlier: why was user None ? Here, find_user() falls through its loop without a match and explicitly returns None — so either target_id is wrong, or that user genuinely isn't in the list yet. The fix, step by step Read the attribute name in the error ( 'name' here) — that tells you which line and which access failed, nothing more. Trace back to where the None value came from. Find the line that assigned, returned, or fetched it. Ask why it's None there , specifically. The most common causes: a lookup function that found nothing and returned None , a dict.get() call th
AI 资讯
Getting British Spelling Instead of American Spelling From AI
You put “use British English spelling” in the system prompt. The first three paragraphs are fine. By paragraph nine there is a color , and by the end there is an organization . The instruction was not ignored; it was outvoted. The symptom The characteristic pattern is not uniform failure. It is a document that starts correct and degrades — and the degradation is usually inconsistent within the document, so you get colour in one paragraph and color two paragraphs later, sometimes in the same sentence as behaviour . Long outputs are worse than short ones, and a long conversation is worse than a single call. A second symptom is domain-specific: the spelling holds in ordinary prose and fails in technical contexts. Code comments, API field names, CSS properties and library names are American by convention ( color is a CSS property; serialize is what the method is called), and text near them pulls the surrounding prose across. Both patterns point at the same cause, and it is not that the model did not read the instruction. Why it drifts back Each token is sampled from a distribution conditioned on everything in the context. The system prompt is part of that context, but so are the two thousand tokens the model has generated since, and so is the enormous prior from training data in which American spelling outnumbers British by a wide margin in almost every technical domain. At the start of a response the instruction is close by and there is little else in the context, so it dominates. As the response grows, the local statistics of the text being generated carry more weight relative to a single instruction several thousand tokens back. And the drift is self-reinforcing in exactly the way described in mid-answer code-switching : once one American spelling is in the context, the conditional probability of the next one rises. The key insight for fixing it is that spelling is not a mode the model is in. There is no British-English state that gets set and then holds. Each word i
AI 资讯
Fixing "TooManyRequests" From Azure OpenAI Under Load
HTTP 429 from Azure OpenAI is four different problems sharing one status code. Three of them are fixed by backing off and one is not, and the response headers distinguish them in about a line of code. Most teams skip that line and file a quota increase for a condition that would have cleared on its own. The error The SDK surfaces it as a rate-limit error — openai.RateLimitError in Python, a RequestFailedException with Status == 429 in .NET. The message text is the first discriminator, and Microsoft documents the indicator phrases rather than a single fixed string: "Requests to … have been limited" or "Rate limit is exceeded" "The service is temporarily unable to process your request" or "System is experiencing high demand" Those two groups mean opposite things. The first is your allocation; the second is Azure’s capacity. Log the message body on every 429 — without it you are guessing. Microsoft, Manage Azure OpenAI quota . Four causes wearing one status code Rate limit exceeded. Your traffic genuinely passed the deployment’s TPM or RPM allocation. Remedy: raise the deployment’s TPM, rebalance quota from an underused deployment, or request an increase. System capacity throttling. Backend capacity is constrained. Documented as often transient. Remedy: retry after the delay the service gives you. A quota increase does nothing here. Temporary rate limit adjustment. The one worth knowing about. Standard and Global Standard deployments share a resource pool across customers, and Microsoft documents that when demand approaches capacity limits the system may temporarily reduce your deployment’s effective rate limit to keep the pool reliable. Your configured quota has not changed. The adjustment typically resolves within a few hours. Token budget consumed by parameters. The rate-limit calculation includes max_tokens and the prompt estimate, not the tokens actually generated. A request with a large max_tokens spends that budget whether or not it uses it. Two more mechanics e
AI 资讯
My Comment-Reply Pipeline Picks One Winner Per Thread. Two Commenters Broke That.
reply_comments.py is the script that tells me which DEV.to comments still need a reply. It walks every comment tree on every article I've published and reports the ones I haven't answered yet. I've fixed two bugs in it already: needs_reply() used to think a thread was "handled" forever after a single reply, even if the other person followed up again, and a dedup check was keyed on the thread's root comment instead of whichever message actually needed the reply, so a second round of conversation went permanently invisible. Both fixes are in --selftest now, and both looked, from the outside, like they'd covered this file's tree-walking logic pretty thoroughly. They hadn't. Today I found a third bug in the same handful of functions, and it survives even with both prior fixes applied. What the existing code assumes Comments on DEV.to come back from the API as trees. A top-level comment has a children list, and each child can have children of its own. The function that decides whether a thread needs attention is needs_reply() , built on latest_message() : def latest_message ( comment ): """ The most recently created message anywhere in this comment ' s subtree. """ latest = comment for c in comment [ " children " ]: candidate = latest_message ( c ) if candidate [ " created_at " ] > latest [ " created_at " ]: latest = candidate return latest def needs_reply ( comment ): return latest_message ( comment )[ " user " ][ " username " ] != ME This walks the whole subtree and returns exactly one message: whichever one has the latest timestamp, anywhere in the tree. _pending_entry() (the function pending() actually calls) is built directly on top of that single answer — it checks whether the latest message needs a reply, and if so, returns one entry for the whole thread. That's a reasonable design if a thread only ever grows one message at a time: root comment, my reply, their follow-up, my reply, and so on. Every test case in this file's --selftest , and both of the earlier bug
AI 资讯
A Space Before the `=` in My .env File Made a Credential Silently Disappear
I have four different load_env() functions in my MCP server project ( my-git-manager ) — one in server.py , one in publish_devto.py , one in reply_comments.py , one in scripts/list_all_published_titles.py . All four exist for the same dumb reason: this repo has no dependency on python-dotenv , so each script that needs GITHUB_TOKEN or DEV_TO_API reads .env by hand. I went digging for a fresh bug in this repo this week — I write a lot about it, and the well is getting shallow — and decided to actually diff all four load_env() implementations against each other instead of reading them one at a time like I usually do. They'd never been compared side by side before. That's how I found this one. The line that started it Every one of them does roughly this: for line in f : line = line . strip () if " = " in line and not line . startswith ( " # " ): k , v = line . split ( " = " , 1 ) os . environ . setdefault ( k , v . strip (). strip ( '"' ). strip ( "'" )) Look closely at what gets .strip() ed there. v — the value — gets stripped of whitespace and surrounding quotes. k — the key, the actual name of the environment variable — gets nothing. That's fine if your .env file looks like this: DEV_TO_API = abc123 It's not fine if it looks like this: DEV_TO_API = abc123 Spaces around = are a completely normal thing to type. Plenty of .env examples online use them. Plenty of people reach for that style out of habit from other config formats. And line.split("=", 1) doesn't care — it splits on the first = no matter what's next to it, so k comes out as "DEV_TO_API " , trailing space included. What that trailing space actually does os.environ.setdefault("DEV_TO_API ", "abc123") sets an environment variable. It's just not the one anything is looking for. Every caller in this repo does os.environ.get("DEV_TO_API") — no trailing space, because that's the name everyone actually types. That lookup returns None , or whatever was already sitting in the environment before .env ever got read. I
AI 资讯
I spent twenty hours testing hypotheses about a publishing failure. The platform had written the reason on screen
Yesterday I tried to publish an article on a writing platform I use. The click did nothing. Not an error, not a refusal: the dialog stayed open, the page changed to a url containing the word submission, and nothing appeared publicly. I tried again. Same. Then I stopped, because I have a rule against stacking attempts, and started diagnosing properly. What I did over the next twenty hours I checked whether the button was disabled. It was not: no disabled attribute, no aria-disabled, pointer events enabled, full opacity, not covered by another element. I checked whether my test for success was valid. I was verifying by loading the post's short url in a clean session and looking for a Not Found. It occurred to me that I had never confirmed that url form works for a published post, so I tested it against one that had published fine an hour earlier. It rendered in full. The test was sound. I checked the public profile. The post was not listed. Confirmed unpublished. I instrumented the network. Enabled the protocol domain, clicked, and watched: three requests, all returning two hundred. So the click was firing and the server was answering without error. That eliminated a dead button, a lost click and an overlay in one measurement, which felt like progress. I formed a hypothesis and wrote it down as a hypothesis: a daily publishing limit, three per calendar day, since two had gone out that day. I waited for midnight and tested it. It failed again. So the hypothesis was refuted, cleanly, and I recorded that. Where the answer was In the dialog. The whole time. After the failed attempt past midnight, I ran one more read of the page, this time asking for elements with an alert role rather than for the button state. One came back: The author of this story has published or scheduled the maximum of two stories in the past 24 hours. Please try to publish or schedule again in 24 hours. Two per rolling twenty four hours. Not three, and not per calendar day. My hypothesis was wrong o
AI 资讯
Silent Retries and Agent Latency: What Sentry's Span Hierarchy Taught Us About Multi-Agent Observability
Sarvar's post about discovering a hidden retry in a 5-agent pipeline (one agent taking 22.6s while others took 5s) is a perfect case study in why observability infrastructure matters for agentic systems. Here's what jumped out: Agent-as-black-box is dangerous. When you string together multiple agents, you lose visibility into retry logic, backoff strategies, and cascade failures unless you instrument at the span level. The latency wasn't in the agent logic itself; it was in the retry envelope. Span hierarchy exposes the invisible. Sentry's approach of grouping spans hierarchically made the problem visible at a glance. Without it, you'd see "agent took 22.6s" and assume it was compute-bound. With hierarchy, the retry pattern was obvious. This scales badly across agents. In a 5-agent system, one bad retry strategy can block or cascade. Add error handling, timeout logic, and fallback chains, and you're building a retry forest no one fully understands. The observability debt compounds. The fix is cheap, the insight is priceless. Once Sarvar knew what was happening, tuning retry counts or backoff curves took minutes. The time cost was finding it. Takeaway: If you're building multi-agent systems, instrument early. Span-level observability isn't optional; it's the difference between "it's slow" and "here's why, and here's the fix."
AI 资讯
Why stock backtesting results deviate: The hidden pitfalls of API timestamp handling
When building and validating US stock quantitative strategies, I used to focus solely on core market data metrics. Like most individual quantitative developers, I prioritized the integrity of price candlesticks and trading volume data, assuming that complete K-line datasets would guarantee reliable backtesting outcomes that align with real-market performance. This assumption held true for small-scale tests and short-cycle verification, until I encountered persistent inconsistencies between historical backtest reports and live trading results. After thorough troubleshooting of strategy logic, parameter settings, and sliding point simulation, I finally pinpointed the root cause — inconsistent and inaccurate timestamp processing from market data APIs, a trivial-looking but critical engineering detail that most developers overlook. Most engineering teams devote massive effort to verifying the accuracy of US stock API quote data, yet ignore standardized processing for time fields. In quantitative trading systems, timestamp offset and timezone disorder are far more impactful than superficial chart display errors. They directly distort candlestick combinations, disrupt technical indicator calculations, and ultimately mislead the entry and exit signal judgments of trading strategies. Core Requirement: Time-series consistency for valid backtesting Market data is essentially a continuous time-series stream, where price and volume merely represent transaction outcomes at specific timestamps. The time dimension acts as the fundamental anchor that defines the exact position of every single trade in the market timeline. Unlike A-share market data that adopts a unified time standard, US stock data providers deliver multiple incompatible time formats across different APIs, including pure UTC time, US Eastern trading time, and original exchange timestamp fields. Without unified parsing and conversion logic in your program, timestamp misalignment and data dislocation are inevitable.
AI 资讯
Debugging is also clicking 🖱️
In the last couple of posts I let agents debug over DAP — breakpoints, step over, continue. That's real debugging. But it's only half of it. When I debug something for real, I also click : I press the button and watch what happens, read the dialog, notice the toggle is greyed out. No backtrace ever tells you the Save button never enabled. So — can the agent do that half too? The web is the easy case Browsers are automatable by design. Most agent tools ship their own browser or drive an external one; point Playwright at a page and every element has a stable, queryable handle. The DOM is an accessibility tree wearing a different hat — roles, labels, structure, all there for the reading. For the web, this half of debugging is close to solved. Native apps are another game There's no DOM. When the agent has nothing to go on, it falls back to the eyeball approach: take a screenshot, let the model look, maybe run OCR or a pre-analysis pass to label what's on screen. It works — and sometimes it's the only option — but it's brittle (a few pixels off and the click misses) and it burns tokens describing pictures. I ran into this by accident. I once wrote a tiny skill whose only job was to screenshot a running 4D form and stitch an animated GIF for a README — 4d-capture-gif . Then I noticed Claude Code reaching for it to debug : the skill also reports a bit of the form's structure — where the buttons are — so the agent knows where to click. For simple cases it genuinely works. But screenshots-plus-coordinates is not the thing I want to build on. The cleaner path: read the tree, don't look at pixels Instead of staring at the screen, read the UI tree directly. On macOS you can script the Accessibility API from Python (pyobjc), and there are automation libraries to help. Now you're clicking element #37, the "Save" button instead of coordinate (412, 260), and hope . A couple of open-source tools are pushing exactly here: agent-desktop — a native CLI that exposes any app's accessibi
AI 资讯
My Commit-Message Script Has 8 Assertions in --selftest. None of Them Touch the Code That Can Actually Fail.
I have three files in this repo that shell out to something over the network or a subprocess and can fail in interesting ways: publish_devto.py , server.py , and git_commit.py . Two of them have --selftest blocks that stub the risky call and exercise the actual failure branches. One doesn't, and I only noticed because I went looking for a reason to be suspicious of my own test coverage after seeing a trending post about counting assertions in a test suite and not liking what you find. git_commit.py reads a staged diff and calls claude -p to turn it into a commit message. It has five distinct exit paths, all guarding real failure modes I've hit before in this project: try : diff = subprocess . check_output ([ " git " , " diff " , " --staged " ], text = True , timeout = 20 ) except subprocess . TimeoutExpired : print ( " git diff --staged timed out after 20s " , file = sys . stderr ) raise SystemExit ( 1 ) if not diff . strip (): print ( " Nothing staged. Run `git add` first. " ) raise SystemExit ( 1 ) try : raw = subprocess . check_output ( [ " claude " , " -p " , " --safe-mode " , SYSTEM + " \n\n " + diff ], text = True , timeout = 20 , stderr = subprocess . PIPE , ). strip () except subprocess . TimeoutExpired : print ( " claude -p timed out after 20s " , file = sys . stderr ) raise SystemExit ( 1 ) except subprocess . CalledProcessError as e : print ( f " claude -p exited { e . returncode } : { ( e . stderr or '' ). strip ()[ : 200 ] } " , file = sys . stderr ) raise SystemExit ( 1 ) except FileNotFoundError : print ( " claude CLI not found on PATH " , file = sys . stderr ) raise SystemExit ( 1 ) That's a held index lock hanging git diff , an empty staging area, a claude -p call that times out, one that exits non-zero, and one where the claude binary isn't even on PATH . Real scenarios — the timeout on this exact git diff --staged call was itself a bug I'd already found and fixed once ( docs/project_notes/bugs.md , 2026-08-06: a prior fix claimed to add a timeout