Releases: davorinpavlica/claude-code-queue
Release list
v0.4.4 — the queue actually runs again
v0.4.3 did not run queued tasks. A task queued for the 00:10 window sat PENDING for 13 hours across two windows. If you are on v0.4.3, upgrade.
The daemon never asked when the task was due
The wait loop released a batch only once NOW >= anchor + 120s, so for a 00:10 reset it would act at 00:12. But the anchor is replaced with the next reset as soon as the stored one is past, which happened at 00:11. From 00:12 the daemon saw "next reset 05:10, keep waiting" and never released anything.
The trigger window was two minutes wide. The anchor closed it after one.
A task already stores the reset it was scheduled for in RUN_AFTER, and that fact does not depend on the anchor. The daemon now releases when either the anchor's window has opened or a task's own RUN_AFTER is past. That is the load-bearing guarantee: any future anchor defect can now only make the queue late, never make it skip a window.
The 2026-08-01 runaway was never fixed, only reshaped
The 30-minute drift threshold holds a later reset_at and then adopts it once the gap crosses 31 minutes. Holding does nothing to stop the projection advancing, so the gap re-crosses every 31 minutes and the anchor steps forward with the clock. Measured: 10:20, 10:51, 11:22, 11:53, 12:24. Net drift identical to the original runaway, only the shape changed, from a ramp to a staircase — which is why 30-minute log samples made it look stable.
It stopped only when a task ran and gave the session real usage. That is the discriminator a gap cannot provide: a session with usage reports a commitment that holds still, an idle session reports a projection that slides. Over the threshold, utilization now decides. Absent is not zero, so the cookie and API paths keep the old behaviour rather than freezing.
The tests that should have caught it
423 tests were green through both failures. Every suite cut daemon.sh at # ── Main loop and tested only the functions above it, so the code deciding when a task runs had no test at all — and every timing test was a snapshot of one moment, which cannot show drift.
tests/test-e2e-window.shruns the real main loop against a simulated clock and a fakeclaude. Sleeps advance simulated time instead of waiting, so a five-hour window passes in milliseconds. Verified against the pre-fix commit: the section replaying the production failure fails there and passes here.tests/test-time-series.shcalls the timing functions over hundreds of consecutive polls with the clock moving, asserting on the sequence rather than a value.scripts/release-check.shand apre-pushhook block a tag push unless every suite is green, with the end-to-end suite required rather than merely included — a missing e2e file fails the gate instead of quietly passing. This release was the first to go through it.
463 assertions across 11 suites, up from 423 across 8.
Also
docs/RESET-TIME.md claimed the drift threshold was "correct under either reading". That was never measured and was false, and being read as settled is why the anchor was ruled out as a suspect for six hours. It now carries both failures with their measurements, and a rule: a claim without the measurement supporting it is unverified, whatever it asserts.
Upgrading
Run ./install.sh. It links release-check.sh and installs the release gate. No data formats changed and existing queued tasks keep working.
v0.4.3 — queued results deliver themselves
Finished tasks used to wait in the inbox until you remembered to type /queue-resume. Now they come to you.
Results deliver themselves
Two hooks hand a finished task back into the chat, both filtered to the project it was queued for:
SessionStart— open or resume a chat on that project and the result is already there.UserPromptSubmit— chat already open, so it arrives with your next message.
/queue-resume is unchanged and still works on demand. All three go through one shared claim, so the automatic paths can never show less than the manual command.
The automatic path used to be the lossy one
Injection already existed, but it read last-output.md, a single file every completion overwrote. Two tasks finishing in one window meant only the last was shown; a result for project A followed by one for project B meant A's was filtered out of A's chat and then gone. That file is retired. Everything now flows through the per-task, per-project inbox that /queue-resume already used.
Two defects caught in review, before either shipped
A task would have eaten its own project's pending results. Every queued task runs as claude --print inside its project directory, which is a new session, so both delivery hooks fire there. Claiming is destructive, so each task run would have silently consumed every pending result for that project into a session nobody reads. The hook docs say print mode does not fire these hooks. A canary planted in the inbox proved otherwise. Both runners now set QUEUE_TASK_RUN=1 and both hooks refuse to deliver when they see it.
Answers containing a markdown rule were truncated. Blocks were separated by \n---\n and split on that same string, so any answer with a --- line broke in two. You saw only the text before the rule; the tail matched no project and sat in the inbox forever. Blocks now split on where a record starts, which no answer body can fake. This one predates the release and affected /queue-resume too.
Also in this release
- The claim can no longer lose what it takes: the working file is per-process and is restored on failure instead of deleted, and a completion landing mid-claim is merged rather than overwritten.
nudge-session.shputs a one-click pickup in the chat you queued from, via the desktop app'spromptSuggestion. It reads an undocumented app-internal store, so every step is fail-open and a failure can never affect a task.⚠️ BUDGET OVERno longer appears at 28% utilization. A closing window is now reported as a clock issue, not a budget one, and four further defects found reviewing that fix are included.
385 tests, up from 264.
Upgrading
Run ./install.sh. It links three new scripts and registers the SessionStart hook. No data formats changed and existing queued tasks keep working.
v0.4.2 — desktop app cookies, a status line that tells the truth
The status line was lying, and now it isn't
statusline.shonly ever read an OAuth token from~/.claude/.credentials.jsonor theClaude Code-credentialsKeychain item. If you sign in through the Claude desktop app rather than the CLI, that token does not exist anywhere, so every refresh failed silently and the gauge served whatever was last cached. On the author's machine that was a file frozen since 24 May, showing a believable, permanent0%.- When no usable token is found, the status line now falls back to the session cookie through
fetch-usage.shand gets the same figures from the same usage endpoint. The cache records which path it used,"api"or"cookie". - A failed refresh can no longer damage a good cache.
The Claude desktop app is now a cookie source
- New
claude-app-cookie.pyreadssessionKey,cf_clearanceandlastActiveOrgfrom the desktop app's own cookie store, decrypting them with theClaude Safe StorageKeychain item. One-time approval of the macOS prompt is required, exactly like the Chrome path. - It is tried before Firefox and Chrome. It is the one client you are certainly signed into, and it refreshes its own session, whereas the browser paths quietly depend on claude.ai still being open and logged in somewhere. If it is unavailable, the chain falls through to the browsers unchanged.
- This fixes the queue and the status line from the same place.
Window times stop flickering between two minutes
- The reported
resets_atjitters about a second either side of the true boundary, and crosses it in both directions. One reset of12:10:00Zcame back as12:09:59.019,12:09:59.512,12:10:00.190and12:10:00.422on consecutive reads, from the meter and the API alike. .next_resetonly storesHH:MM, and every conversion truncated, so the displayed hour flipped between14:09and14:10at random. Real resets land on whole minutes, so the value is now snapped to the nearest minute at both places an epoch enters the system.- v0.4.1 fixed a real but different rounding bug in the
/usagetext fallback. It was not the cause of the flicker anyone actually saw; this is.
Notes
- 264 tests, up from 240. New: 21 for jitter and the desktop-app cookie source, plus a new
tests/test-statusline.shsuite. install.shlinks the new helper. Nothing on disk changes format, and existing queued tasks keep working.
🤖 Generated with Claude Code
v0.4.1 — window times no longer one minute early
Patch on v0.4.0. Window times could show one minute early and stay that way.
- The
/usagetext fallback reads a phrase that is cut off at whole minutes. "Resets in 4 hr 15 min" really covers anything up to 4h15m59s, so computing exactly 4h15m landed up to 59 seconds early, which writes as a full minute early. - On its own that was self correcting, because the next live reading replaced it. The sticky anchor added in v0.4.0 changed that: a live time only one minute later now counts as a slide and is held, so the wrong minute stuck for the whole window.
- The fallback now rounds up to the next whole minute. Real reset times land on whole minutes, so this recovers exactly what the phrase cut off.
- Rounding up is also the safe direction. An earlier live time is always adopted, so a late guess corrects itself while an early one does not.
Observed before the fix: the meter reported 14:10:00 while /queue-status held 14:09 across repeated calls. After: 14:10, stable.
v0.4.0 — window bars, task sizes, correct reset anchor
/queue-status shows you the shape of your day
- Five windows (
Q1–Q5) render as capacity bars with absolute clock times, not countdowns.Q1 [████████░░] 75% · danes 14:10 ☀️tells you when the window opens and how much of it is already spoken for. - Each task line names which project it belongs to, so a queue shared across repos is readable at a glance:
└─ PENDING - pivoledar: ~10% preveri druge editorje. - Tasks that will not fit are marked
← auto-shift to Q2before the daemon moves them, so the shift is never a surprise.
Size a task when you queue it
--small/-s,--medium/-m,--large/-lon/queue-task. The estimate feeds the capacity bars and the auto-shift decision.- Without a flag, the queue uses the measured average of your previous tasks, exactly as before.
Window times are correct again
v0.3.0 anchored window hours on the live meter. That was half right, and the missing half cost real time.
- The meter's
reset_atmeans two different things. During an active session it is a commitment that does not move. With no active session it is a projection that slides forward with the clock. - Following the projection made the daemon chase a moving target: on 2026-08-01 its wake time moved 30 minutes every 30 minutes, and a task queued at 13:24 did not run until 20:44.
- The anchor is now sticky. A later reset time within 30 minutes is treated as a slide and ignored. Beyond 30 minutes it is a real new window and is taken. Earlier times are always taken, so the queue never delays on its own guess.
- Result: the hour shown in
/queue-statusnow matches whatrusted-claude-meterand claude.ai show, and it stops moving while you work.
Notes
- 234 tests, up from 155. Timing logic keeps the four mandatory scenarios and adds six for the threshold, including a replay of the 2026-08-01 runaway.
- No changes to
tasks.mdon disk. Existing queued tasks keep working.
v0.3.0 — /queue-resume, fill limit calculator, live reset anchor
Results come back to you
/queue-resume— new command. Pulls finished task results into the current chat. Filtered by project, so a task queued in one repo never surfaces in another repo's chat. Results accumulate between calls; nothing is lost.- Every finished task also leaves an HTML archive in
~/.claude/queue/outputs/, and its output is copied to the clipboard.
The queue warns you before a window is full
calc-fits.sh— estimates how many tasks still fit in the current 5-hour window, learning from the measured cost of your previous tasks (task-costs.log, last 50)./queue-taskand/queue-statusshow that estimate. TIGHT at 70% utilization, OVER at 85% or under 30 minutes to reset.- The chat warns you directly when a window is nearly full, at most once every 30 minutes.
- The daemon auto-shifts a task to the next window when it will not fit, and marks it BLOCKED after 3 shifts instead of retrying forever.
Window times are now correct
- Window hours (q1–q5) anchor on the live meter instead of a cached value that drifts stale during a session. Before this,
/queue-statuscould be several minutes early and--qNwrote those wrong minutes into the queue. - The daemon confirms the reset actually happened before running a batch. This also stops a task from firing mid-window after the Mac has been asleep.
-q2(single dash) is now accepted alongside--q2.
Notes
rusted-claude-meterstays optional. Without it, everything falls back to the cached reset time exactly as before.- New file
scripts/reset-source.shis a library, not a command.install.shplaces it for you.
v0.2.0
New reset-time sources, chosen automatically instead of hardcoded:
- rusted-claude-meter support — if rusted-claude-meter is installed and running, its exported reset time is used directly (highest priority, no cookies or browser needed).
- Chrome cookie auto-import (
chrome-cookie.py, via thebrowser_cookie3library) alongside the existing Firefox path, no DevTools, no manual copy-paste, on either browser. - Browser order is decided at runtime, never hardcoded: your macOS default browser first, then whichever cookie store was modified more recently, with a manual override available if you want to force one.
- Reset times more than 6 hours in the future are now rejected as implausible, from any source.
See CHANGELOG.md for the full v0.1.0 → v0.2.0 history.
Full Changelog: https://github.com/davorinpavlica/claude-code-queue/commits/v0.2.0