Build1 distinct publisher3 min readUpdated
A scheduled Claude Code job skipped three runs in a row and reported success every time. The cheap fixes: check whether the PID in the lock file is alive, and alert on output rather than status.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A scheduled content generator on one operator's Claude Code setup produced nothing for roughly two hours while reporting success: three consecutive launchd runs, zero items generated, exit 0 every time, and not one alert [1]. The cause, per the writeup, was a process that had already been killed but whose pid, 94799, was still sitting in the lock file at `~/dev/brand-404/sns/gen_work/.lock`, so every later run concluded a job was in flight and exited cleanly [2].
The script does have a stale-lock escape hatch. Line 57 sets `LOCK_STALE_SEC = 2 * 3600`, which permits a run to steal the lock if the file's mtime is older than two hours [3]. The original implementation looked only at mtime and never asked whether the recorded pid was alive [4]. That means the safety valve, not the monitoring, defined the outage: 7,200 seconds of skipped runs, ending because a timer expired rather than because anything noticed the corpse [5].
Orphaned locks are the normal case here, not an exotic one. A SIGKILL skips the `finally` block, so `release_lock()` never runs, and the same thing happens when you stop a job by hand during development [6].
The lock is there for a real reason. A single run makes up to three `claude -p` calls under a MAX plan quota, with paid APIs excluded, plus image generation, and takes tens of minutes; `CLAUDE_TIMEOUT` is 600 seconds [7][17]. Three calls at that ceiling is 30 minutes of model timeout budget alone, before downloads [8]. Without mutual exclusion, a scheduled restart can overlap an unfinished run, double-generate the same brand, and produce concurrent writes [9]. The pipeline in between is not trivial either: queue check, brand selection from `brand-catalog.json`, a Shopify `/products.json` fetch, copy generation, image role selection, slide build, self-QA, then output to a feature directory and lock release [16].
The reason two hours passed unnoticed is the buffer. `QUEUE_TARGET = 3` and `MAX_GEN_PER_RUN = 2`, and the author's stated intent is that three items in stock keep posting alive through a day or two of failed generation [10]. A separate job posts one item daily at 19:30 through the Instagram Graph API and logs it to `state/ig_posted.jsonl` [11]. Three queued items against one post per day is roughly three days of cover [12]. The inventory that protects the visible output is the same thing that conceals the broken half of the system.
The stakes are the author's own and unaudited: six months of building, and a claim that the pipeline now supports JPY 1.2M a month in sales unattended [13], against an earlier manual stretch of 30 straight days of hand-posting at JPY 100k a month that ended in burnout [14]. The generalisable part is the framing: a script is correct when written, and time turns dependencies, dead processes and leftover lock files into debris that blocks every future run, so an autonomous setup needs a repair layer [15].
Two things to check in your own scheduler this week. First, whether lock acquisition tests pid liveness rather than file age alone, because age-only logic converts a crash into a silent timed outage [4][5]. Second, whether your alerting keys on artifacts produced rather than process status, since three runs exiting 0 delivered three consecutive false all-clears [1][18]. A queue depth that has not moved is a signal; a return code from a process that decided to do nothing is not.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The author reports that on a Claude Code autonomous environment, three consecutive launchd runs generated zero pieces of content, each with last exit=0, with no alert, over about two hours.
The cause was process pid 94799, already killed, still holding the lock file ~/dev/brand-404/sns/gen_work/.lock; every subsequent run was skipped with a 'lock held' message and generation stopped for about two hours.
gen_feature.py line 57 sets LOCK_STALE_SEC = 2 * 3600, a safety valve allowing a run to steal the lock if the lock file's mtime is older than two hours.
The original implementation only checked the lock file's mtime and did not check whether the recorded pid was alive, so a killed pid with a recent mtime kept being read as 'still running'.
If the script is killed instantly with SIGKILL, the release_lock() in the finally block never runs at all; the same happens when the job is stopped by hand during development.
A single run takes tens of minutes, using up to three claude -p calls plus image generation, with CLAUDE_TIMEOUT = 600 seconds.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Strong code-level detail, single unverified source
The technical core is unusually well documented for a blog postmortem: the failing and fixed acquire_lock() bodies are quoted, constants are cited by line number (LOCK_STALE_SEC line 57, QUEUE_TARGET line 60, MAX_GEN_PER_RUN line 61), and the end-to-end pipeline is diagrammed. But everything comes from one publisher and one author, the log evidence is described rather than shown, and the outcome claims (revenue, unattended operation) are unverifiable, which caps the score well below high confidence.
One personal production pipeline
Observed adoption is a single operator's own environment: one launchd-scheduled generator, one daily Instagram autoposter, one disclosed Claude MAX quota usage pattern, plus the incident and its fix. No other deployments, users, downloads, or third-party uptake of the pattern are reported, so adoption is real but minimal in scope.
Solid narrow finding, oversized framing
The diagnosed mechanism is modest and well evidenced — one missing PID liveness check in one script — while the framing scales it into a thesis about autonomous environments needing a repair layer and attaches an unverified ¥1.2M/month revenue stake. The gap is positive but not extreme, because the article does not overstate the fix itself and openly shows the code; the overstatement is in generalization and monetary stakes, not in the engineering.
Practitioner self-promotion, no vendor disclosure
The author has a clear incentive to present an impressive, self-sustaining Claude Code money machine: the piece foregrounds personal revenue figures and a burnout-to-automation arc typical of audience-building developer content on dev.to. No sponsorship, vendor relationship, or affiliate arrangement is disclosed in the supplied source, and the technical content is verifiable in-place, so the incentive pressure is on the framing rather than on the engineering claims.
Mechanism trustworthy, outcomes unconfirmed
Confidence is moderate: the failure mode, root cause and fix can be evaluated directly from quoted code and named constants, and they are internally consistent with the reported two-hour bound and the queue buffer. Confidence is held down by the single-source cluster, the absence of raw logs, and business claims that cannot be checked at all.
build
A Retention Policy for Agent Memory: Flag Unused Skills at 30 Days, Archive at 901 distinct publisher
build
Config rot has a latency bill: a 70-line weekly audit for agent environments1 distinct publisher
build
The 46GB Leak Your RSS Alert Cannot See: macOS Compressed Memory Breaks Threshold Monitoring1 distinct publisher
build
Screenshot retrieval gets a one-line install, and the comparison is text embeddings1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 18, 2026