CLI, workflow helpers, Worker code, tests, and simulation fixtures for coordinating concurrent agent work.
A zero-config first run uses a clearly labeled local store so one person can
try the CLI immediately. The team and multi-machine runtime path is the HTTP
backend: AGENT_COORD_API_URL points the CLI at the Cloudflare Worker backed by
D1, and AGENT_COORD_API_TOKEN authenticates this machine to that Worker. The
legacy GitHub backend is available only when explicitly requested by
maintainers.
Keep this public repository code-only. Do not commit live claims/,
heartbeats/, batches/, events/, *.json.lock, secrets, environment files,
customer data, credentials, or source-code patches here.
This repository is the MIT License protocol plane for Agent Coordination. That plane includes the CLI, Cloudflare Worker API, Worker-served read-only dashboard, simulation harness, tests, documentation, ADRs, and examples.
The runtime state is private data, not product source. Live claims, heartbeats, batches, events, lock files, tokens, credentials, customer data, and source-code patches must stay outside this repository.
A future hosted or monetized ShakaStack product plane can use a different
license and repository boundary. Product-plane dashboards and batch-planning
features should consume the protocol-plane API rather than relicense the
protocol primitives in this repository. The standalone
shakacode/agent-coordination-dashboard repository should carry the same MIT
protocol-plane stance while it remains the local/protocol dashboard. See
ADR 0002.
gh auth status
gh repo clone shakacode/agent-coordination
cd agent-coordination
bundle install
git config core.hooksPath .githooks
bundle exec rubocop
ruby -Itest test/agent_coordination_cli_test.rb
bin/agent-coord --help
bin/agent-coord bootstrap
export PATH="$HOME/.local/bin:$PATH"
agent-coord --help
agent-coord status
agent-coord demoThe versioned pre-commit hook in .githooks/pre-commit runs RuboCop on staged
Ruby files before each commit after core.hooksPath is configured. CI runs the
full RuboCop check on every pull request.
The CLI supports Ruby 3.2 or newer. This repository pins a current Ruby version for development and CI also runs the Ruby suite on the supported floor.
The agent-coordination RubyGem installs only the agent-coord CLI and its
public documentation; the Worker deployment remains source-only. The gem has not been published.
Build and install it locally to verify the distribution without changing a
registry, tag, or release:
gem build agent-coordination.gemspecReplace VERSION with the version in the filename printed by gem build.
gem install --local ./agent-coordination-VERSION.gem
agent-coord version --json
rm ./agent-coordination-VERSION.gemGenerated .gem files are local artifacts and should not be committed. See the
Changelog for release-facing changes and the
Worker state protocol with curl for placeholder-only
HTTP examples.
Run agent-coord status without configuring a backend. The CLI uses
$XDG_STATE_HOME/agent-coordination when XDG_STATE_HOME is an absolute path.
Relative, empty, or unset values use ~/.local/state/agent-coordination.
Every command that selects this implicit default prints its path with a
local mode — single-machine only notice. JSON commands keep machine-readable
output on stdout; the notice goes to stderr.
This default is for one machine only. Configure the HTTP backend before sharing coordination state across machines or operators.
Run the deterministic walkthrough to see the claim and heartbeat model without configuring or changing any persistent backend:
agent-coord demoThe demo uses an isolated temporary local store, shows a live-holder claim refusal followed by stale and dead heartbeat states and a successful takeover, then removes its temporary state. It ignores configured HTTP and legacy GitHub backends, so it never writes demo data remotely.
Run this once for each Cloudflare environment before provisioning machine tokens:
cd worker
npm install
npx wrangler login
npx wrangler d1 create agent-coord
# In wrangler.toml, replace the all-zero database_id with the ID printed above.
# Keep that deployment-specific substitution out of commits.
npx wrangler d1 migrations apply agent-coord --remote
npx wrangler deploy
export AGENT_COORD_API_URL=<worker-url>
curl -fsS "$AGENT_COORD_API_URL/v1/health"
cd ..Keep deployment credentials and generated tokens out of git. The CLI only needs the deployed Worker URL and a machine token at runtime.
Provision one token per machine from the repository root. The command prints the token once and stores only its SHA-256 hash in D1, so run it in a private terminal:
worker/bin/provision-token <machine-name> \
--read-prefix <read-prefix> \
--write-prefix <write-prefix>Token provisioning requires at least one read or write prefix. An omitted scope
dimension receives no access ([]). Use repeatable flags when a machine needs
multiple path scopes:
worker/bin/provision-token m5 \
--read-prefix claims/shakacode/react_on_rails \
--read-prefix heartbeats \
--write-prefix claims/shakacode/react_on_rails \
--write-prefix heartbeats/m5-codex.jsonFor a trusted single-operator deployment that intentionally needs unrestricted
access, pass --all-state instead of prefix flags:
worker/bin/provision-token <machine-name> --all-stateThe stored empty scope ("") grants all state, but the provisioning command
does not accept an empty prefix as a shortcut; all-state access must use the
explicit flag. A directory scope such as
claims/shakacode/react_on_rails covers descendant paths. A valid record-path
scope such as heartbeats/m5-codex.json covers exactly that flat record. The
Worker enforces read scopes for GET /v1/state/<path> and
GET /v1/state?prefix=..., write scopes for PUT /v1/state/<path>, and records
the authenticated machine as updated_by on each state write. Claim takeover
checks may need read access to the current holder's heartbeat; use an exact
heartbeat write scope only when the machine's agent id is stable.
When listing a parent prefix above a scoped token's read scope, the Worker
returns only covered descendants. Claims-scoped tokens can pass the default
agent-coord doctor read probe; tokens scoped only to other prefixes should use
agent-coord doctor --doctor-prefix <read-prefix>. Directory prefixes are
checked with the list endpoint; exact record-path prefixes such as
heartbeats/m5-codex.json are checked with a record read. The provisioning
script rejects a command with no scope flags and rejects combining prefix flags
with --all-state. Read-only tokens support status and doctor workflows.
Write-only tokens support append-only callers such as record-event; claim,
release, heartbeat, and batch mutation commands read existing state before
writing and therefore need matching read prefixes.
For local Wrangler/D1 development, pass --local:
worker/bin/provision-token dev --local \
--read-prefix claims \
--write-prefix claimsMachine names may contain letters, numbers, dots, underscores, colons, and
hyphens. If wrangler d1 execute fails, the script preserves Wrangler's output
and, when the failure looks like a duplicate-machine or token constraint, adds a
hint to delete or update the existing D1 machines row before re-provisioning.
After the Worker is deployed and this machine has a token, set both HTTP backend env vars and verify the backend:
export AGENT_COORD_API_URL=<worker-url>
export AGENT_COORD_API_TOKEN=<machine-token>
agent-coord doctorBackend selection follows this rule:
--state-rootflag ->LocalStore--api-urlflag orAGENT_COORD_API_URLenv ->HttpStoreAGENT_COORD_STATE_ROOTenv ->LocalStore--backendflag orAGENT_COORD_BACKENDenv -> legacyGitHubStore- otherwise -> labeled local store at the zero-config path above
When both AGENT_COORD_API_URL and AGENT_COORD_STATE_ROOT are set, the CLI
uses the HTTP backend and warns once. Pass --state-root only for an explicit
local smoke check.
React on Rails workflow docs assume agent-coord is available on PATH.
bin/agent-coord bootstrap installs agent-coord into $HOME/.local/bin by
default and appends that directory to the current shell profile. Use
--install-dir PATH to choose another directory or --no-profile to skip
profile edits. If the shell has not reloaded the profile yet, export the path in
the active terminal:
export PATH="$HOME/.local/bin:$PATH"Run agent-coord doctor after setup. The default doctor is intentionally
lightweight: it verifies backend access and the expected state layout without
downloading and parsing every JSON record. On an unconfigured first run it
initializes and verifies the zero-config local root. Run
agent-coord doctor --deep for a full audit that parses every claim, heartbeat,
and batch file. If an explicitly configured backend fails, agents should report
coordination state
as UNKNOWN and use the public claim-comment fallback until the operator fixes
backend access.
For HTTP tokens scoped outside claims, pass a readable scope:
agent-coord doctor --doctor-prefix events/<batch-id>.
To override the default for a local smoke check, set AGENT_COORD_STATE_ROOT or
pass --state-root to use a temporary filesystem state directory:
STATE_ROOT=$(mktemp -d)
AGENT_COORD_STATE_ROOT="$STATE_ROOT" agent-coord heartbeat \
--agent-id worker-3969 \
--repo shakacode/react_on_rails \
--target 3969 \
--batch-id batch-2026-06-13 \
--branch jg-codex/3969-agent-coord-backend
AGENT_COORD_STATE_ROOT="$STATE_ROOT" agent-coord status
rm -rf "$STATE_ROOT"bin/agent-coord claim --agent-id ID --repo OWNER/REPO --target ISSUE_OR_PR [--batch-id ID] [--branch BRANCH] [--metadata options] [--ttl SECONDS]
bin/agent-coord release --agent-id ID --repo OWNER/REPO --target ISSUE_OR_PR [--metadata options] [--handoff-to ID] [--handoff-note TEXT]
bin/agent-coord heartbeat --agent-id ID [--repo OWNER/REPO] [--target ISSUE_OR_PR] [--batch-id ID] [--branch BRANCH] [--metadata options] [--status STATUS]
bin/agent-coord register-batch --file PATH
bin/agent-coord record-event --batch-id ID --type TYPE [--lane NAME] [--agent-id ID] [--repo OWNER/REPO] [--target ISSUE_OR_PR] [--branch BRANCH] [--status STATUS] [--metadata options] [--message TEXT]
bin/agent-coord status [--json]
bin/agent-coord status --repo OWNER/REPO --target ISSUE_OR_PR [--json]
bin/agent-coord status --batch-id ID [--json]
bin/agent-coord version [--json]
bin/agent-coord config [show] [--json]
bin/agent-coord doctor [--json] [--deep] [--doctor-prefix PREFIX] [--state-root PATH]
bin/agent-coord bootstrap [--install-dir PATH] [--profile PATH] [--no-profile]
bin/agent-coord demo
demo is a deterministic, isolated local walkthrough. It does not use backend
environment variables, make remote requests, or preserve its temporary state.
claim acquires or renews a lease. If an active claim exists for another agent,
the holder's heartbeat is the normal liveness source: live or stale
heartbeats refuse takeover, while a dead heartbeat allows takeover. If the
holder heartbeat is missing or invalid, expires_at is the safe fallback and the
claim can be taken over only after that fallback has passed. Existing claim
updates use the active store's compare-and-swap token, so competing updates fail
instead of silently overwriting each other.
Metadata options available on claim, heartbeat, and release are
--thread-handle, --chat-handle, --host, --pr-url, --dashboard-url,
--operator, --phase, --generation, and --instance-id. These fields are
additive, optional, and included in JSON status output when present. Workers use
them to connect a lane, chat, host app, branch, PR, operator, and dashboard deep
link without parsing handoff prose.
release --handoff-to ID --handoff-note TEXT is the structured handoff path for
moving work between agents, hosts, machines, or operators. The released claim is
stamped with release_mode: "handoff" plus handoff_to and handoff_note, so
the next claimant can recover the branch, PR, phase, and resume note from the
target-scoped record. When the claim has a batch_id, release also appends a
handoff event to events/<batch-id>/; the event is best-effort because the
released claim itself is the durable handoff source.
register-batch --file PATH validates and writes a JSON batch manifest to
batches/<batch-id>.json using the active store. It stamps schema_version,
registered_at, and updated_at, preserves optional operator/dashboard/thread
metadata, and rejects malformed lane names or owner/target fields before workers
claim lanes.
record-event appends immutable batch or lane events under
events/<batch-id>/<event-id>.json. Use it for phase changes and noteworthy
operator-visible milestones that should remain visible even when a heartbeat is
overwritten by the next phase. Event records accept the same optional metadata
fields as claims and heartbeats, plus --type, --lane, and --message.
release --handoff-* creates handoff events automatically when a batch id is
available; use direct record-event --type handoff only for non-release
breadcrumbs.
heartbeat upserts heartbeats/<agent-id>.json. status renders coordination
state in text or JSON. Full status renders compact claims, heartbeats, batch
lanes, lane dependencies, blocked-on refs, and recent events for broad audits.
Scoped status is the preferred batch-workflow path:
status --repo OWNER/REPO --target ISSUE_OR_PRreads onlyclaims/<owner>/<repo>/<issue-or-pr>.jsonand that claim holder's heartbeat when a holder exists.status --batch-id IDreads onlybatches/<id>.json,events/<id>/, lane-owner heartbeats, and dependency batch files plus referenced lane-owner heartbeats needed to computeblocked_on.
Scoped JSON payloads include scope and degraded fields. A scoped command can
show degraded notes for intentionally omitted unrelated state, such as claims
not checked in batch scope; that is different from exit 2. Exit 2 means the
coordination backend result is UNKNOWN for that command. Text status renders the
same degraded notes as a footer when rows are present. In large backends, prefer
target or batch scoped status for React on Rails batch lanes and treat a timed
out full coordination read as degraded/UNKNOWN rather than guessing.
release marks a claim released while preserving the record for auditability.
Only the recorded holder can release or restamp metadata on an existing claim;
another agent should claim the target after release instead of re-releasing the
old holder's record. For planned ownership moves, include --handoff-to and
--handoff-note on the original release, then have the next worker claim the
same repo/target and continue on the recorded branch/PR.
version prints the CLI contract version. config show --json prints runtime
defaults and machine-readable exit codes. Default doctor verifies the current
backend without writing state or parsing every record; doctor --deep adds full
JSON validation. For HTTP tokens whose read scope does not overlap claims, use
doctor --doctor-prefix <read-prefix> to verify that scoped read path.
bootstrap installs the agent-coord command used by public
workflow docs.
Use agent-coord version --json and agent-coord config show --json as the
stable contract for public workflow docs. Public repos should avoid copying
private implementation defaults when they can point agents at these commands.
Current exit code contract:
| Exit | Meaning | Agent behavior |
|---|---|---|
| 0 | Command succeeded | Use the returned state. |
| 1 | Usage error | Fix the command invocation before proceeding. |
| 2 | Operational failure | Report coordination state as UNKNOWN; use advisory fallback when safe. |
| 3 | CLAIM_REFUSED by live/stale/active hold |
Hard stop for machine agents; report holder/liveness instead of competing. |
A refused claim is intentionally different from a bootstrap/auth/network
failure. A machine agent may not override exit 3 on its own. Exit 2 means the
backend could not be trusted for that command, including storage-level compare
and-swap contention; dependency-sensitive lanes should stop with UNKNOWN until
the coordinator restores backend access.
Heartbeat liveness is derived from timestamps:
now < expires_at->liveexpires_at <= now < updated_at + 4 * ttl->stalenow >= updated_at + 4 * ttl->dead
ttl is the interval between updated_at and expires_at. Use short heartbeat
TTLs, normally 15 minutes. A stale heartbeat is a warning that the agent may be
thinking, offline, or between tool calls. A dead heartbeat means claims held by
that agent are recoverable.
Workers should refresh heartbeats at every phase transition: item start, branch or PR update, review pass, blocked state, and done state. Long-running desktop sessions should also use the platform scheduler templates so liveness does not depend on the agent being between tool calls.
The launchd/com.shakacode.agent-coord-heartbeat.plist.example template refreshes
one heartbeat every 5 minutes. Install one heartbeat job per live batch lane:
export AGENT_ID=m5-codex-batch2
export TARGET_REPO=shakacode/react_on_rails
export TARGET=3970
export BATCH_ID=agent-coord-2026-06-13
export BRANCH=jg-codex/3969-agent-coord-backend
export AGENT_COORD_REPO="$(pwd)"
export AGENT_COORD_ENV_FILE="$HOME/.config/agent-coord/env"
mkdir -p "$(dirname "$AGENT_COORD_ENV_FILE")"
install -m 600 /dev/null "$AGENT_COORD_ENV_FILE"
cat > "$AGENT_COORD_ENV_FILE" <<'EOF'
AGENT_COORD_API_URL=<worker-url>
AGENT_COORD_API_TOKEN=<machine-token>
EOF
perl -pe 's#__AGENT_ID__#$ENV{AGENT_ID}#g;
s#__TARGET_REPO__#$ENV{TARGET_REPO}#g;
s#__TARGET__#$ENV{TARGET}#g;
s#__BATCH_ID__#$ENV{BATCH_ID}#g;
s#__BRANCH__#$ENV{BRANCH}#g;
s#__AGENT_COORD_ENV_FILE__#$ENV{AGENT_COORD_ENV_FILE}#g;
s#__AGENT_COORD_REPO__#$ENV{AGENT_COORD_REPO}#g' \
launchd/com.shakacode.agent-coord-heartbeat.plist.example \
> "$HOME/Library/LaunchAgents/com.shakacode.agent-coord-heartbeat.${AGENT_ID}.plist"
launchctl bootstrap "gui/$(id -u)" \
"$HOME/Library/LaunchAgents/com.shakacode.agent-coord-heartbeat.${AGENT_ID}.plist"You can also replace the __PLACEHOLDER__ values manually. Keep the env file
private (chmod 600) and never commit it. The checked-in template loads
AGENT_COORD_API_URL and AGENT_COORD_API_TOKEN from that local file instead
of storing token values in the repository.
The systemd/agent-coord-heartbeat.service.example template runs the same
heartbeat loop under systemd --user. Install one service per live batch lane,
substituting the same placeholders used by the launchd template:
mkdir -p "$HOME/.config/systemd/user"
sed -e "s#__AGENT_ID__#${AGENT_ID}#g" \
-e "s#__TARGET_REPO__#${TARGET_REPO}#g" \
-e "s#__TARGET__#${TARGET}#g" \
-e "s#__BATCH_ID__#${BATCH_ID}#g" \
-e "s#__BRANCH__#${BRANCH}#g" \
-e "s#__AGENT_COORD_ENV_FILE__#${AGENT_COORD_ENV_FILE}#g" \
-e "s#__AGENT_COORD_REPO__#${AGENT_COORD_REPO}#g" \
systemd/agent-coord-heartbeat.service.example \
> "$HOME/.config/systemd/user/agent-coord-heartbeat.${AGENT_ID}.service"
systemctl --user daemon-reload
systemctl --user enable --now "agent-coord-heartbeat.${AGENT_ID}.service"The systemd template loads the same private env file for
AGENT_COORD_API_URL and AGENT_COORD_API_TOKEN.
Runtime state lives in these directories:
claims/<owner>/<repo>/<issue-or-pr>.json
heartbeats/<agent-id>.json
batches/<batch-id>.json
events/<batch-id>/<event-id>.json
The checked-in .gitkeep files only preserve the directories. Schema examples
are documented below rather than committed as live JSON records, so status
does not show fake work.
{
"schema_version": 1,
"repo": "shakacode/react_on_rails",
"target": "3969",
"agent_id": "worker-3969",
"batch_id": "batch-2026-06-13",
"branch": "jg-codex/3969-agent-coord-backend",
"thread_handle": "batch13-backend-quokka",
"host": "codex",
"operator": "justin",
"phase": "claimed",
"generation": 3,
"instance_id": "m5-codex-20260708T180000Z",
"status": "active",
"claimed_at": "2026-06-13T00:30:00Z",
"updated_at": "2026-06-13T00:30:00Z",
"expires_at": "2026-06-13T04:30:00Z"
}Required fields: schema_version, repo, target, agent_id, status,
claimed_at, updated_at, expires_at.
Allowed status values for the initial lifecycle are active and released.
Coordinators should treat a claim holder with a dead heartbeat as recoverable
even if the claim expires_at timestamp is still in the future. expires_at
remains useful for audit and as the fallback when the heartbeat is missing or
invalid.
Optional lane metadata fields on claims are thread_handle, chat_handle,
host, pr_url, dashboard_url, operator, phase, generation, and
instance_id. release preserves the existing claim record and the recorded
holder may update the same metadata fields for terminal states, such as adding a
final pr_url or phase.
{
"schema_version": 1,
"agent_id": "worker-3969",
"repo": "shakacode/react_on_rails",
"target": "3969",
"batch_id": "batch-2026-06-13",
"branch": "jg-codex/3969-agent-coord-backend",
"thread_handle": "batch13-backend-quokka",
"host": "codex",
"pr_url": "https://github.com/shakacode/react_on_rails/pull/3969",
"dashboard_url": "https://coord.example.test/batches/batch-2026-06-13/backend",
"operator": "justin",
"phase": "validating",
"generation": 3,
"instance_id": "m5-codex-20260708T180000Z",
"status": "in_progress",
"updated_at": "2026-06-13T00:40:00Z",
"expires_at": "2026-06-13T00:55:00Z"
}Required fields: schema_version, agent_id, status, updated_at,
expires_at.
Optional lane metadata fields on heartbeats are thread_handle, chat_handle,
host, pr_url, dashboard_url, operator, phase, generation, and
instance_id. Status readers should treat missing metadata as UNKNOWN rather
than inferring it from branch names or handoff text.
{
"schema_version": 1,
"event_id": "20260708T235500.123456Z-deadbeef",
"batch_id": "batch-2026-06-13",
"type": "phase",
"lane": "docs",
"agent_id": "worker-3972",
"repo": "shakacode/react_on_rails",
"target": "3972",
"branch": "jg-codex/3972-docs",
"thread_handle": "thread-docs",
"host": "codex",
"operator": "justin",
"phase": "validating",
"message": "running tests",
"at": "2026-06-13T00:42:00Z"
}Required fields: schema_version, event_id, batch_id, type, and at.
Lane events should include lane and agent_id when available. Lane names
follow the same rules as registered batch lanes: non-empty and no :
characters, because dependency refs split on the last colon. Event ids are
time-sortable and unique per write.
The current HTTP backend stores events in the same JSON state API as claims,
heartbeats, and batches, so events/<batch-id> is intended for low-volume phase
transitions and audit breadcrumbs, not high-frequency telemetry. Keep event
volume bounded per batch until the relational /v1/events endpoint in
backend-design.md replaces the interim JSON store.
The interim Worker state listing is resumable: GET /v1/state?prefix=... keeps
the historical full-snapshot response, while callers may pass limit and then
follow next_cursor with the same prefix to read additional pages. Prune or
export released claims, expired heartbeats, and old batch/event records before
prefix snapshots become expensive.
{
"schema_version": 1,
"batch_id": "batch-2026-06-13",
"repo": "shakacode/react_on_rails",
"objective": "Ship backend and docs updates",
"operator": "justin",
"dashboard_url": "https://coord.example.test/batches/batch-2026-06-13",
"lanes": [
{
"name": "backend",
"owner": "worker-3969",
"targets": ["3969"],
"thread_handle": "thread-backend",
"host": "m5",
"pr_url": "https://github.com/shakacode/react_on_rails/pull/3969",
"depends_on": []
},
{
"name": "docs",
"owner": "worker-3972",
"targets": ["3972"],
"thread_handle": "thread-docs",
"host": "m1",
"depends_on": ["batch-2026-06-13:backend"]
}
],
"registered_at": "2026-06-13T00:30:00Z",
"updated_at": "2026-06-13T00:30:00Z"
}Required manifest fields before registration: batch_id and non-empty lanes.
register-batch writes schema_version, registered_at, and updated_at.
Each lane should include name, owner, and targets. owner is the stable
agent id used by heartbeat, so status can attach the lane's latest heartbeat
status and liveness. Lane names must not contain :; batch ids may contain :.
depends_on is optional and accepts a string or array of lane refs in the form
<batch-id>:<lane-name>, split at the last colon.
Top-level batch metadata such as repo, objective, instructions, launch_prompt,
operator, dashboard_url, and lane metadata such as thread_handle, chat_handle,
host, pr_url, dashboard_url, operator, and phase are preserved and
included in JSON status output. A dependency is considered met when the
referenced lane owner's heartbeat reports a terminal status such as done,
complete, completed, merged, or ready. A released claim is preserved for
auditability and does not unblock dependent lanes by itself. Unmet dependencies
appear in the lane's blocked_on field:
batches
- batch-2026-06-13
- lane backend owner worker-3969 targets 3969 status in_progress live deps - blocked_on -
- lane docs owner worker-3972 targets 3972 status blocked live deps batch-2026-06-13:backend blocked_on batch-2026-06-13:backend
Workers with unmet dependencies should set their own heartbeat to blocked,
switch to another independent lane, and check agent-coord status again before
resuming, rebasing, or pushing dependency-sensitive work.
- Coordinator registers a batch manifest describing lanes and dependencies.
- Worker acquires a claim for its issue or PR target.
- Worker refreshes a heartbeat during active work and records phase events for milestones that should remain visible after later heartbeats overwrite state.
- Coordinator uses targeted
status --repo ... --target ...orstatus --batch-id ...for lane decisions, and fullstatusonly for broad audits where an all-state scan is acceptable. - Worker releases the claim or lets the lease expire if the session is lost.
Keep leases short enough that abandoned work is recoverable, usually 2-4 hours for active batch claims and 15 minutes for heartbeats.