Run Jobs In Parallel On A Repo
By default the daemon runs queued jobs one at a time per repo: --workers
defaults to 1 and the scheduler defaults to barrier (a per-tick batch that
serializes same-repo jobs on one checkout lock). That is the safe, conservative
default — but it means a fan-out of independent jobs on one repo runs serially
unless you opt in to parallelism.
This guide shows the one-step on-ramp.
The short version
# Intent-level: workers + the pool scheduler together, named for the goal.
gitmoot daemon start --parallel 5
# Equivalent, explicit form:
gitmoot daemon start --workers 5 --scheduler pool
--parallel N sets --workers N and --scheduler pool in one flag. It is
sugar for the explicit pair and cannot be combined with --workers or
--scheduler (that would be ambiguous).
If you raise --workers above 1 and do not pass --scheduler, the daemon now
auto-selects pool for you — requesting multiple workers under the serializing
barrier is almost never what anyone wants:
gitmoot daemon start --workers 5 # auto-selects --scheduler pool
To keep the old per-tick semantics with multiple workers, ask for them
explicitly — an explicit --scheduler barrier is always honored:
gitmoot daemon start --workers 5 --scheduler barrier
Confirm the daemon is configured for it
gitmoot daemon status reports the scheduler mode and worker count, so you can
answer "is the daemon actually set up for the parallelism I asked for?" without
re-deriving it from launch flags:
daemon running pid 12345
log: ~/.gitmoot/logs/daemon.log
scheduler: pool, workers: 5
claude auth: ok (...)
If you started under barrier with multiple workers, the line flags it and points
at the fix.
Preflight warning
If a parallelizable workload is queued under a serializing config, the daemon logs an actionable warning with the exact relaunch command instead of silently serializing the work:
warning: 3 parallelizable jobs queued for owner/repo will run serially under the
current scheduler config; relaunch with: gitmoot daemon restart --parallel 3
"Parallelizable" is counted conservatively: same repo, dependency-unblocked (already true of queued jobs), and distinct runtime sessions (see the next section). Two jobs on the same agent session are not counted as parallelizable, because they serialize regardless of the scheduler.
The warning is rate-limited: it is re-logged only when the parallelizable set changes, not on every poll, so a steady backlog does not spam the daemon log.
What actually runs in parallel (two serialization layers)
Parallelism under pool is bounded by two independent locks, not one:
-
Checkout lock (per worktree). Jobs run concurrently only when they have distinct checkout keys. Delegation / orchestra
implementchildren already get their own worktree from the workflow engine, so they parallelize. Read-onlyask/reviewjobs can be auto-isolated into an ephemeral detached worktree. A plain, top-level same-repoimplementjob with no worktree still shares therepo:<repo>key and serializes even underpool.Auto-isolated read-only worktrees are the committed tip. An auto-isolated read-only worktree is a detached
git worktree addat the committed tip of the base branch, so it does not contain gitignored paths (e.g. vendored clones underrepos/**) or any uncommitted working-tree changes. Isolation only kicks in when a same-repo read-only job is contended — a delegation fan-out of two or more read-only siblings, or two-plus independently-fired top-levelask/reviewjobs on one repo; a single, uncontended read-only job stays in the shared base checkout and sees everything. Every auto-isolated job's prompt carries a note with the canonical base-checkout absolute path, so a worker whose sandbox can read it (e.g. codex) reaches the real tree instead of reporting a working-tree feature as missing. For whole-working-tree analysis that must see gitignored or uncommitted state, either keep the job uncontended (a lone read-only job stays in the base checkout) or pass an absolute path to the file/dir under analysis. -
Runtime session lock (
runtime:<runtime>:<ref>). Two jobs that use the same agent/runtime session serialize on the session lock even if their checkouts differ. So same-repo parallelism is bounded by distinct runtime sessions — give the work distinct sessions (or distinct agents) to spread it across the pool. This applies to resumable runtimes (Codexthread_id, Claude, Kimi); the checkout lock is runtime-agnostic, the session lock is not.
The practical recipe for N-wide same-repo work today is therefore: N
delegation/orchestra legs (each engine-isolated into its own worktree) running
under distinct runtime sessions, with --parallel N.
Reconfigure without restarting (SIGHUP)
Changing workers/scheduler/poll/idle cadence no longer needs a daemon restart (#577):
kill -HUP <daemon-pid> re-reads the [daemon] config section (poll,
workers, scheduler, parallelism, idle_grace_ticks, and
idle_max_multiplier) live — no teardown, no dropped jobs, no
environment re-inheritance (so the daemon's runtime auth is untouched). Values
pinned by explicit launch flags win over the re-read config. When a full
restart is genuinely needed, prefer gitmoot daemon restart, which recovers
the per-delivery Claude auth file.
Cap one repo's parallelism from config
To cap a single repo on a shared daemon — without touching the global worker
count or relaunching anything — add a [repos."owner/repo"] section (#576):
[repos."owner/repo"]
max_parallel = 1 # cap this repo's in-flight jobs; 0/unset = global default
# scheduler = "barrier" # optional per-repo scheduler override
max_parallel = 0 (or an absent section) means "use the global default"; a
positive value caps that repo's concurrent jobs. The keys are re-read every
tick, so edits apply live.
Host-wide admission budget
On a memory-constrained host, the opt-in [admission] section adds a second,
host-global gate the daemon applies before starting each agent session, on
top of --workers/pool and the per-repo locks (#365):
[admission]
max_concurrent_sessions = 0 # cap total in-flight sessions; 0 = off
max_memory_gb = 0 # cap summed per-runtime RAM estimate; 0 = off
# codex_memory_gb = 0.2 # operator-tunable per-runtime RAM priors
# claude_memory_gb = 0.85
# kimi_memory_gb = 0.5
# default_memory_gb = 0.5
With both caps 0 (the default) the budget is disabled and scheduling is
byte-identical to a config without the section. A job that does not fit BOTH
caps is left queued and retried next tick — never failed — so "jobs stay
queued for no visible reason" on a small host can mean the admission budget is
holding them. The budget is enforced per daemon process (host-global for the
normal single-daemon deployment).
GitHub rate-limit-aware scheduling
The daemon's polling and every agent's gh/API calls share one GitHub account.
GitHub's secondary (abuse-detection) rate limit fires on burstiness and
concurrency, not total volume, so concurrent bursts can trip it (HTTP 403
"secondary rate limit") and freeze all GitHub ops even while the primary quota is
fine — the only manual workaround being to stop the daemon and wait out the
cooldown (#683).
The opt-in [github] section installs a GitHub call budget + adaptive backoff
that is in-process to the daemon — it covers the gh/API calls gitmoot itself
issues from the daemon process (polling, comments, merges, status). It is enforced
per daemon process (host-global for the normal single-daemon deployment), the same
scope as the admission budget above. It does not reach into separate foreground
processes (a foreground gitmoot orchestrate/pool/review/pr comment) or the
gh calls a codex/claude runtime subprocess makes on its own — those run outside the
daemon process and never touch the shared limiter.
[github]
max_concurrent = 0 # cap in-flight gh calls; 0 = unlimited (default)
min_interval = "0s" # min spacing between call starts; 0 = off (Go duration or bare seconds)
secondary_backoff = true # pause all GitHub calls on a secondary/abuse limit (default true)
backoff_base = "60s" # exponential fallback base when no Retry-After (default 60s)
backoff_max = "5m" # exponential fallback cap (default 5m)
conditional_requests = true # ETag conditional polling (default true)
calls_per_hour_warn = 0 # daemon-local sliding-hour warning; 0 = off
Safe defaults: the proactive caps (max_concurrent, min_interval) default
off, so single-call latency and steady-state throughput are unchanged; only the
reactive secondary_backoff is on, and it is invisible on the happy path — it
engages only after a gh call actually fails with a secondary/abuse limit.
On a hit the limiter pauses all GitHub calls process-wide (respecting the
response's Retry-After, else the exponential fallback) rather than retry-storming
the abuse detector, which only prolongs the block. Calls are never dropped — they
queue/delay until the window passes. On a busy host, set max_concurrent (e.g.
6) and/or a small min_interval (e.g. 250ms) to also smooth bursts
proactively. gitmoot daemon status shows the configured budget.
The four per-tick repository list reads use an in-memory ETag cache. A 304 Not Modified replays the prior raw JSON and does not consume GitHub's REST quota.
After three consecutive successful all-304 ticks, a quiet repo moves to 2x base
cadence and then to 4x; configure the thresholds under [daemon]:
[daemon]
idle_grace_ticks = 3
idle_max_multiplier = 4 # 1 disables idle decay
Any response-body miss, poll error, queued repo job, or in-flight repo job resets
the streak and promotes the repo immediately. A repo with an open PR remains at
base cadence because its per-PR comment reads are deliberately non-conditional.
The decayed NextPoll gates GitHub calls only: heartbeat, pipeline, and chat
maintenance still wake at the resolved base interval. The local call count is
approximate and covers only this daemon process; foreground commands and
agent-owned gh processes are outside it.
Not yet automatic (follow-ups)
- Top-level
implementauto-isolation.pooldoes not auto-isolate plain same-repoimplementjobs into worktrees — only read-onlyask/reviewjobs. Parallelizing independent top-levelimplementjobs via the daemon needs implement-eligible auto-isolation (a real branch worktree + branch-lock handling- a worktree cap and disposal sweep); that is intentionally deferred.
See also
- CLI reference —
daemon start/daemon runflags. - Coordinator recipes — orchestra fan-outs whose legs are already worktree-isolated.