ã¯ã©ãŒã¿ã埩æ§ããç¬éã15æ¬ã®ãžã§ããã©ãã ãåå®è¡ãã¹ãã â launchdã¹ãããéžå¥ãããã®èšèš
ååãWikiã®secret-scanåæãçŽããè©±ãæžããŸãããä»åã¯æ¯è²ãå€ããŠãclaude-quota-guard.py ãšããŠçµãã ã¯ã©ãŒã¿ã®ãµãŒããããã¬ãŒã«ãŒã®ç¶ãã§ãããã®ã¹ã¯ãªããã¯ã¯ã©ãŒã¿æ¯æžãæ€ç¥ããŠå
šãžã§ããæ¢ãããšããã§è©±ãçµãã£ãŠããŸããããå®éã«éçšãããšãã®å
ã«å¥ã®åé¡ããããŸãããµãŒããããéããåŸãã¹ããããããåãã©ããããã§ãã
å°ãããšïŒãµãŒããããéããŠããã¹ãããããããžã§ãã¯æ»ã£ãŠããªã
claude-quota-guard.py ã®run_jobã¯ããµãŒããããéããŠããéã«ãžã§ããèµ·åãããšå³åº§ã«exit 0ã§è¿ãããã°ã«ããŒã«ãŒã ãæ®ããŸãã
print(
"CLAUDE_QUOTA_JOB_SKIPPED "
f"job={label} reason={status['reason']} remaining={status['remaining_seconds']}s ts={now()}",
file=sys.stderr,
)
return 0
ãã®ã¬ãŒãã¯~/Library/LaunchAgents/*.plistã®ãã¡15æ¬ã«ããã£ãŠããŸããã³ãŒãäžã®ã³ã¡ã³ãã«ããéãã§ãã
# ðŽ 2026-08-21: circuit ãéããšå
šãžã§ããäžåŸã§æ¢ãŸããããæ¶è²»ã®å€§åãå ãã
# è¿ä¿¡/ãšã³ã²ãŒãžç³»ãã¯ã©ãŒã¿ã䜿ãåã£ãå·»ãæ·»ãã§ãæçš¿ããŸã§åæ¢ããŠããã
åé¡ã¯ããµãŒããããéããŠopen_untilãéããŠããlaunchdã¯æ¬¡ã«ãã®ãžã§ãã®StartCalendarIntervalãæ¥ããŸã§äœãããŠãããªãããšã§ããæ9:00ã®ãžã§ããã¯ã©ãŒã¿ã§ã¹ãããããã10:00ã«åŸ©æ§ããŠããæ¬¡ã®ã¹ããããç¿æ9:00ãªãäžž1æ¥åã®å®è¡æ©äŒã倱ãããŸãããã®ã埩æ§åŸã«äœããã©ãã ãåå®è¡ãããããæ±ºããã®ãquota-catchup.pyã§ãã
èšèšïŒ3段ã®çµã蟌ã¿ã§åè£ã決ãã
quota-catchup.pyã¯find_candidatesã§å
šplistãèãã3ã€ã®æ¡ä»¶ãANDã§éãããã®ã ããåå®è¡åè£ã«ããŸãã
| æ¡ä»¶ | å€å®é¢æ° | ç®ç |
|---|---|---|
| ãã®plistã¯ã¬ãŒã察象ã | is_guarded | ç¡é¢ä¿ãªãžã§ããæ··ããªã |
| 仿¥ã®ããŒã«ãŒãSKIPPEDã | latest_marker_is_today_skip | ãã§ã«å®è¡æžã¿ã»å€ãã¹ããããé€å€ |
| éããã¹ããããããã | calendar_slot_passed | StartIntervalãžã§ããæªæ¥ã¹ãããã®ã¿ã®ãžã§ããé€å€ |
def find_candidates(...) -> list[Candidate]:
candidates: list[Candidate] = []
for plist_path in sorted(launch_agents.glob("*.plist")):
plist = load_plist(plist_path)
if not plist or not is_guarded(plist):
continue
...
if label in already_kicked or not latest_marker_is_today_skip(output_paths, label, today):
continue
if calendar_slot_passed(plist, now):
candidates.append(Candidate(label, plist_path))
return candidates
äºéèµ·åãé²ãïŒis_guarded
åå®è¡å¯Ÿè±¡ã¯ãclaude-quota-guard.pyçµç±ã§èµ·åããŠããããžã§ãã«éå®ããŸããå€å®ã¯ProgramArgumentsã®æååäžèŽã ãã§ãã
def is_guarded(plist: dict) -> bool:
arguments = plist.get("ProgramArguments")
return isinstance(arguments, list) and any("claude-quota-guard" in str(value) for value in arguments)
ã¬ãŒããçµç±ããŠããªãplistãŸã§æŸããšãã¯ã©ãŒã¿ãšç¡é¢ä¿ãªæ®éã®cronãžã§ããåæã«kickããŠããŸããŸãã
ãéããã¹ãããããããããæ±ºããïŒcalendar_slot_passed
ãããä»åã®äž»åœ¹ã§ããStartCalendarIntervalã¯1ã€ã®ãžã§ãã«è€æ°ã¹ããããæãŠãŸããå®äŸãcom.shun.daily-brief.plistã§ãã
<key>StartCalendarInterval</key>
<array>
<dict><key>Hour</key><integer>8</integer><key>Minute</key><integer>0</integer></dict>
<dict><key>Hour</key><integer>10</integer><key>Minute</key><integer>30</integer></dict>
</array>
8:00ã®æ ãã¯ã©ãŒã¿ã§ã¹ããããããŠãã10:30ã«ãŸãåããžã§ããèªç¶ã«èµ°ããŸããã ãããéããã¹ãããã1ã€ã§ãããã°åå®è¡å¯Ÿè±¡ãã§ããããæªæ¥ã®ã¹ããããæ®ã£ãŠããããåŸ
ãŠãã§ã¯ãããŸãããéã«StartIntervalïŒäŸïŒ30åããïŒã®ãžã§ãã¯æŸã£ãŠãããŠã次ã®ã€ã³ã¿ãŒãã«ã§èªç¶ã«åå®è¡ãããã®ã§ãcatchupã®å¯Ÿè±¡ã«ããå¿
èŠããããŸããã
def calendar_slot_passed(plist: dict, now: datetime) -> bool:
"""True only for calendar-only jobs with at least one past slot today."""
if "StartInterval" in plist:
return False
raw_entries = plist.get("StartCalendarInterval")
if isinstance(raw_entries, dict):
entries = [raw_entries]
elif isinstance(raw_entries, list):
entries = raw_entries
else:
return False
saw_today_slot = False
for entry in entries:
if not isinstance(entry, dict) or "Hour" not in entry:
return False
if not runs_today(entry, now):
continue
try:
scheduled = now.replace(
hour=int(entry["Hour"]),
minute=int(entry.get("Minute", 0)),
second=0, microsecond=0,
)
except (TypeError, ValueError):
return False
if scheduled > now:
continue
saw_today_slot = True
return saw_today_slot
å
šã¹ããããæåŸãŸã§èããŠsaw_today_slotãORã§ç©ãã®ããã€ã³ãã§ãããããæåã«èŠã€ãã£ãã¹ãããã§å€å®ãè¿ãããããªæžãæ¹ãããŠããããã¹ãããã®äžŠã³é 次第ã§çµæãå€ãããã°ãèžã¿ãŸããäŸãã°[{9:00}, {18:00}]ãšãã䞊ã³ã§13:00ã«å€å®ããå Žåãæ£ããã¯9:00ãéå»ãªã®ã§å¯Ÿè±¡ã§ãããã«ãŒããæåŸã®ãšã³ããªã ãã§å€å®ããŠããã18:00ãæªæ¥ãšããçç±ã§å¯Ÿè±¡å€ã«èª€å€å®ããŸãã
宿ž¬ãã調æŽããã¿ã€ã ã¢ãŠãïŒJOB_TIMEOUT_SECONDS
åå®è¡ã¯kick_and_waitã§launchctl kickstartããŠãããlaunchdã®ç®¡çäžããå€ããïŒïŒçµäºããïŒãŸã§ããŒãªã³ã°ããŸãããã®ã¿ã€ã ã¢ãŠãã¯æå1200ç§ã§ãããã宿ž¬ã§äžååã§ããã
# 宿ž¬(2026-08-21): affameba-gen çã® claude çæã¬ãŒã³ã¯ 20 åãè¶
ããã
# 1200s ã ãšãåŸ
ã€ã®ããããŠæ¬¡ã kickãããã ãã§åã®ãžã§ãã¯çãç¶ãã
# runbook ãèŠæ±ããçŽå kick ã厩ããŠéãçæãéãªãïŒload 40 è¶
ã®äºæ¬¡è¢«å®³ïŒã
JOB_TIMEOUT_SECONDS = 2700
# timeout æã¯åŸ
ã€ã®ããããã ãã§ãªãå®éã«æ¢ããããããæ®ºããªããšçŽåæ§ãä¿ãŠãªãã
JOB_KILL_GRACE_SECONDS = 30
ãåŸ
ã€ã®ããããããšããžã§ããæ¢ãããã¯å¥ç©ã ããšããã®ãããã®åŠã³ã§ããåè
ã ãã ãšãã¿ã€ã ã¢ãŠãããå€ããžã§ããããã¯ã°ã©ãŠã³ãã§çãç¶ãããŸãŸæ¬¡ã®åè£ãkickããŠããŸããçæãéãªã£ãŠload 40è¶
ãšããäºæ¬¡è¢«å®³ãåºãŸããããªã®ã§terminate_jobã§SIGTERMâSIGKILLã®é ã«å®éã«æ®ºããçŽåæ§ãæ
ä¿ããŠããŸãã
def terminate_job(domain_label: str, label: str) -> None:
"""timeout ãããžã§ããå®éã«æ¢ãããæ¬¡ã® kick ãšéãªãããªãããã®çŽåæ§ã®æ
ä¿ã"""
for signal_name in ("SIGTERM", "SIGKILL"):
subprocess.run(["launchctl", "kill", signal_name, domain_label], capture_output=True, check=False)
deadline = time.monotonic() + JOB_KILL_GRACE_SECONDS
while time.monotonic() < deadline:
pid, _ = launchctl_status(label)
if pid is None:
return
time.sleep(2)
ãã¹ãã§å¢çæ¡ä»¶ã朰ãïŒtest_quota_catchup.py
ãã®ã¹ã¯ãªããã«ã¯18åã®ãã¹ãã±ãŒã¹ããããŸãïŒunittestã§17ãpytestã¹ã¿ã€ã«ã®é¢æ°ã1ïŒãå人ã®èªååã¹ã¯ãªããã«ãããŸã§ãã¹ããæžãã®ã¯ããéãã«èŠãããããããŸããããå®éã«å¹ããã±ãŒã¹ãæããŸãã
åè£ãŒãã®æ¥ã«claudeãå©ããªã
ãã¡ã°ãå®å®³ã倧ããã®ã¯ããã§ããrun颿°ã®ã³ã¡ã³ãã«ãã®çç±ãæžããŠãããŸãã
# æŸããã®ãç¡ãæ¥ã« probe ãæã€ãšã30åããã« claude -p ã1æ¥48å空æã¡ããŠ
# ã¯ã©ãŒã¿ãåãïŒãã®ãžã§ããé²ãããšããŠããäºæ
ãã®ãã®ãèµ·ããïŒã
# åè£ãåºãæã ãå埩ã確èªããã
if not candidates:
return [], 0, 0
ãããå®ããã¹ããtest_no_candidates_skips_probeã§ãã
def test_no_candidates_skips_probe(self):
self.add_job(
error_text=f"CLAUDE_QUOTA_JOB_RAN job=com.lily.test exit=0 ts={int(self.now.timestamp())}",
)
probe = Mock(return_value=True)
kicker = Mock()
result = quota_catchup.run(
dry_run=False, now=self.now, state_path=self.state, catchup_path=self.catchup,
launch_agents=self.agents, log_path=self.root / "result.log", probe=probe, kicker=kicker,
)
self.assertEqual(result, ([], 0, 0))
probe.assert_not_called()
kicker.assert_not_called()
ãã¯ã©ãŒã¿åŸ©æ§ã確èªããããã®ããããã確èªäœæ¥èªäœã§ã¯ã©ãŒã¿ãåãããšããã®ã¯ããµãŒããããã¬ãŒã«ãŒãäœã£ãåŽãªã絶察ã«èžã¿ãããªãèªå·±ççŸã§ããprobe.assert_not_called()ãšãã1è¡ãããããæ©æ¢°çã«ä¿èšŒããŠããŸãã
仿¥ã®ã¹ãããã¯å¯Ÿè±¡ã3æ¥åã¯å¯Ÿè±¡å€
def test_today_skip_is_candidate(self):
label = self.add_job()
self.assertEqual([item.label for item in self.candidates()], [label])
def test_old_skip_is_not_candidate(self):
self.add_job(mtime=self.now - timedelta(days=3))
self.assertEqual(self.candidates(), [])
latest_marker_is_today_skipã¯æ£èŠè¡šçŸã§ãã°ããts=ãæŸããæ¥ä»ã仿¥ãã©ãããèŠãŠããŸãã3æ¥åã®SKIPPEDãã°ã仿¥ãŸãæŸãããããéå»ã®å€±æãæ¯æ¥åå®è¡ãç¶ãããŸã³ãç¶æ
ã«ãªããŸãã
éå»ã¹ããããããã°æªæ¥ã¹ããããæ®ã£ãŠããŠã察象
def test_past_slot_makes_candidate_even_if_later_slot_is_future(self):
self.add_job(schedule=[{"Hour": 9, "Minute": 0}, {"Hour": 18, "Minute": 0}])
self.assertEqual([item.label for item in self.candidates()], ["com.lily.test"])
å
è¿°ããcalendar_slot_passedã®ORå€å®ããå®éã®daily-briefåã¹ã±ãžã¥ãŒã«ïŒè€æ°ã¹ãããïŒã§åºå®ããŠãããã¹ãã§ãã
ããŒã«ãŒã®é åºãåãéããªã
ãã°ã«ã¯SKIPPEDãšRANãæ··åšããŸãããææ°ã®ããŒã«ãŒãã©ã¡ããããèŠèª€ããšãå®ã¯æåæžã¿ã®ãžã§ããäºéã«kickããããéã«æååŸã®å¥ã¹ããããèŠèœãšãããããŸãã
def test_ran_marker_after_skip_in_same_log_is_not_candidate(self):
label = "com.lily.mixed"
today = int(self.now.timestamp())
self.add_job(
label,
error_text=(
f"CLAUDE_QUOTA_JOB_SKIPPED job={label} reason=quota remaining=1s ts={today - 1}\n"
f"CLAUDE_QUOTA_JOB_RAN job={label} exit=0 ts={today}"
),
)
self.assertEqual(self.candidates(), [])
SKIPPEDã®åŸã«RANãããã°ãçµå±ããšã§æåãããã®ã§å¯Ÿè±¡å€ããšããæç³»åå€å®ãlatest_job_markerãreversed(lines)ã§æ
ä¿ããŠããŸãã
ãã®ã¹ã¯ãªããã®åäœãã¹ãã«å ±éããçãã¯ãè³¢ãããžãã¯ã®æ£ãããã§ã¯ãªããçŽ æŽã«æžãããééããå¢çãå ã«åºå®ãããããšã§ããcalendar_slot_passedã®ORå€å®ãããŒã«ãŒã®æ°æ§å€å®ãåè£ãŒãæã®probeæå¶ââã©ããå®è£ 1è¡ã®æžãæ¹æ¬¡ç¬¬ã§çµæãå転ããç®æã§ãåãããŠã¿ããŸã§æ°ã¥ãã«ãããã®ã°ããã§ããå人çšã®èªååã¹ã¯ãªããã«pytestãæžã䟡å€ã¯ãã¬ãã¥ã¢ãŒã説åŸããããã§ã¯ãªããå幎åŸã®èªåã仿§ãå€ãããšãã«åãééããèžãŸãªãããã«ãããŸãã
èžãã èœãšã穎
- 1200ç§ã¿ã€ã ã¢ãŠãã§20åè¶
ã®ãžã§ããæã¡åããã â 宿ž¬ãã2700ç§ã«æ¡åŒµã
JOB_KILL_GRACE_SECONDS=30ã§SIGTERMâSIGKILLã®ç¶äºãç¢ºä¿ - ã¿ã€ã ã¢ãŠãã§ãåŸ
ã€ã®ãããããã ãã ãšçŽåæ§ã厩ãã â
terminate_jobã§å®éã«killããªããšãåã®ãžã§ããçãç¶ãããŸãŸæ¬¡ãkickããŠload 40è¶ ã®äºæ¬¡è¢«å®³ãåºã - åè£ãŒãã®æ¥ã«probeãæã€ãšãé²ãããšããŠããäºæ
ãã®ãã®ãèµ·ãã â åè£ãããæã ã
probe_claude()ãåŒã¶ - StartIntervalãžã§ããŸã§æŸããšäžèŠãªåå®è¡ã«ãªã â
StartIntervalãããã°ç¡æ¡ä»¶ã§calendar_slot_passedã¯False - SKIPPED/RANã®ããŒã«ãŒé åºããååšãããã©ãããã ãã§èŠããšèª€å€å®ãã â
reversed(lines)ã§ææ°ã®ããŒã«ãŒã ããèŠã --dry-runã®ãã³ã«claudeãå©ããšç¢ºèªã³ã¹ããå®è¡ã³ã¹ãã«ãªã â dry-runã¯probeãåŒã°ãåè£åæã ãã§è¿ã
ãŸãšã
- ã¯ã©ãŒã¿ã®ãµãŒããããã¬ãŒã«ãŒã¯ãæ¢ãããã§çµããã§ã¯ãªããéããåŸã«äœãåå®è¡ããããŸã§èšèšããªããšåŸ©æ§ãç¿æ¥åŸ ã¡ã«ãªã
- åå®è¡åè£ã¯
is_guarded/latest_marker_is_today_skip/calendar_slot_passedã®3段ANDãéããã¹ãããã1ã€ã§ãããã°å¯Ÿè±¡ãæªæ¥ã¹ãããã®æç¡ã¯ç¡é¢ä¿ - ã¿ã€ã ã¢ãŠãã¯å®æž¬ïŒ20åè¶ ã®ãžã§ãå®åšïŒããæ±ºããæã¡åãæã¯å®éã«killããŠçŽåæ§ãå®ã
- åè£ãŒããªãprobeããåŒã°ãªãã埩æ§ç¢ºèªã®ããã®ããããèªåã§ã¯ã©ãŒã¿ãåãèªå·±ççŸãé¿ãã
- å人ã¹ã¯ãªããã®åäœãã¹ãã¯ãçŽ æŽãªå®è£ ã ãšå転ããã¡ãªå¢çïŒã¹ãããé åºã»ããŒã«ãŒæ°æ§ã»ãŒãä»¶æã®å¯äœçšïŒãå ã«åºå®ããŠããããã«ãã
次åã¯ããã®ãµãŒãããæ¬äœââclaude-quota-guard.pyããäžéã¡ãã»ãŒãžããšãèšäºæ¬æã«ããŸããŸåãèªãããæåã±ãŒã¹ããã©ãåºå¥ããŠãããããæžãäºå®ã§ãã
LilyïŒ@bokuwalilyïŒâ å人éçºè ãClaude Code ã§èªåååºç€ãçµã¿ãªãããiOSã¢ããªãWebãµãŒãã¹ãéç£ããŠããŸã
- AIã§ãå¯ãŠãŠãåãä»çµã¿ããäœã£ãŠæ120äžã«ãã話㯠noteã®ææèšäº ã«ð°
- OSS: github.com/bokuwalily ð
- ææ°æ å ±ã»ãåãåãã㯠X @bokuwalily ãžð
- AIå°å ¥ã»èªååã®çžè«ãšå®è£ ãã³ãã¬7æ¬ã®é åžã¯ å ¬åŒLINE ããð¬
çããã® â€ïž ãã·ã§ã¢ãå±ã¿ã«ãªããŸãïŒ