paperclip/server/src
levineam 631b7806ed
Add heartbeat preflight budget caps (#8347)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Heartbeats are the control-plane path that turns scheduled,
comment-driven, or on-demand wakeups into adapter executions.
> - Budgeting and recurring work need enforcement before an adapter
starts, not only after model usage is recorded.
> - Empty timer wakes also need an opt-in fast-exit path so operators
can keep routine schedules without paying for no-op model turns.
> - This pull request adds heartbeat preflight gates for daily run and
daily cost caps, plus an explicit timer no-work skip policy.
> - The benefit is safer autonomous operation: capped agents stop before
new execution, queued work is cancelled cleanly at claim time, and
proactive agents still run by default unless the operator opts into
no-work skipping.

## Linked Issues or Issue Description

No public issue exists for this change. Inline bug report:

### What happened?

Heartbeat execution can start without enforcing per-agent daily
invocation and spend limits at the heartbeat boundary. A run that was
queued before a cap was reached can also be claimed later and invoke the
adapter unless the cap is checked again immediately before execution.
Operators also do not have an explicit opt-in fast-exit policy for
generic timer wakes with no actionable assigned work.

### Expected behavior

Configured daily run and daily cost caps should stop new heartbeat runs
before adapter execution. Already queued runs should be rechecked at
claim time and cancelled cleanly when a cap is now reached. Queued issue
runs cancelled by daily caps should release their issue execution locks
and promote deferred wakeups without entering immediate recovery loops.
Generic timer no-work skipping should be opt-in so proactive agents
continue to run by default.

### Steps to reproduce

1. Configure an agent heartbeat policy with a one-run daily cap or a
daily cost cap.
2. Create or queue heartbeat wakeups for that agent after the cap has
already been consumed.
3. Observe that without preflight and claim-time checks, the heartbeat
path can still enqueue or claim work that should be blocked before
adapter execution.

### Paperclip version or commit

Reproduced against `master` before this branch.

### Deployment mode

Local dev (`pnpm dev`) / built from source.

### Installation method

Built from source (`pnpm dev` / `pnpm build`).

### Agent adapter(s) involved

Not adapter-specific (core heartbeat scheduling and claim logic).

### Database mode

External Postgres in tests via embedded test harness.

### Access context

Not applicable.

### Node.js version

Node 20 in CI-compatible local development.

### Operating system

macOS local development, Linux CI-compatible tests.

### Relevant logs or output

The regression suite added in this PR covers the failing paths:

```shell
pnpm exec vitest run server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts
```

### Relevant config (if applicable)

```json
{
  "heartbeat": {
    "maxDailyRuns": 1,
    "maxDailyCostCents": 1,
    "skipTimerWhenNoActionableWork": true
  }
}
```

### Additional context

This affects recurring/autonomous operation because the safest place to
stop excess work is before adapter execution starts.

### Privacy checklist

Reviewed for sensitive data; no private logs, credentials, or local
instance URLs are included.

## What Changed

- Added heartbeat policy parsing for per-agent daily run caps, daily
cost caps, and opt-in no-actionable-work timer skipping.
- Added pre-queue daily cap checks while preserving same-issue wake
coalescing.
- Added claim-time cap checks so already queued runs are cancelled
before adapter execution when a cap is reached.
- Added skipped wakeup metadata for cap and timer fast-exit decisions.
- Released issue execution locks for queued issue runs cancelled by
daily caps, with deferred wake promotion and without immediate recovery
loops while caps are active.
- Added regression coverage for timer skipping, proactive default
behavior, run caps, cost caps, queued-run cancellation, started
cancelled runs, and deferred issue wake promotion.

## Verification

- `git diff --check`
- `node -c server/src/services/heartbeat.ts`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts`
- `pnpm --filter @paperclipai/server typecheck`
- Local autoreview: `skills/autoreview/scripts/autoreview --mode branch
--base origin/master --engine codex --model gpt-5.5 --thinking high`
  - Result: clean, no accepted/actionable findings

## Risks

- Medium operational risk because this changes heartbeat scheduling and
claim-time behavior.
- The no-actionable-work timer fast-exit is explicitly opt-in to avoid
suppressing proactive agents unexpectedly.
- Daily run caps count runs by `startedAt` so old queued rows do not
consume today’s cap, while started runs still count even if they later
end as cancelled.
- Queued issue-run cap cancellation uses the existing release/promotion
path with immediate recovery suppressed to avoid retry loops while caps
are active.

## Model Used

Codex with GPT-5.5 high reasoning assisted with implementation, local
testing, and autoreview. The final review gate used local autoreview
with `gpt-5.5` high reasoning.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-06-19 21:39:50 -07:00
..
__tests__ Add heartbeat preflight budget caps (#8347) 2026-06-19 21:39:50 -07:00
adapters feat(adapters): stamp agent id via X-Anthropic-Agent-Id for claude_local (#8322) 2026-06-19 09:56:28 -07:00
auth [codex] Bundle local branch fixes from PAP-10032 (#6604) 2026-05-25 07:25:26 -05:00
http [codex] Runtime control-plane fixes (#6380) 2026-05-20 10:37:11 -05:00
lib Add low-trust review containment (#7530) 2026-06-05 16:48:02 -05:00
middleware feat(server): TRUST_PROXY supports CIDR list + named subnets (supersedes #3729) (#5872) 2026-06-12 10:37:55 -07:00
onboarding-assets Add workspace file viewer and artifact links (#7681) 2026-06-09 17:17:43 -05:00
realtime
…
routes Fix mention-granted closed issue comments with resume intent (#8350) 2026-06-19 16:47:06 -07:00
secrets fix: validate session ID as UUID before --resume + error diagnostics (DLD-889) (#1742) 2026-06-09 20:21:10 -07:00
services Add heartbeat preflight budget caps (#8347) 2026-06-19 21:39:50 -07:00
storage Address artifact PR review feedback 2026-05-30 20:51:46 +00:00
types
…
agent-auth-jwt.ts feat(security): per-company JWT signing keys for multi-tenant isolation (#5864) 2026-06-11 18:00:22 -07:00
app.ts feat(server): TRUST_PROXY supports CIDR list + named subnets (supersedes #3729) (#5872) 2026-06-12 10:37:55 -07:00
attachment-types.ts Allow inline video attachment previews 2026-06-05 03:28:07 +00:00
board-claim.ts [codex] Add agent permissions and controls plan (#6386) 2026-05-22 08:12:52 -05:00
config-file.ts
…
config.ts
…
dev-runner-worktree.ts [codex] Bundle local branch fixes from PAP-10032 (#6604) 2026-05-25 07:25:26 -05:00
dev-server-status.ts [codex] UI and dev ops quality-of-life (#6384) 2026-05-19 15:52:39 -05:00
dev-watch-ignore.ts
…
errors.ts
…
first-admin-claim.ts [codex] Add private browser first-admin claim flow (#6755) 2026-05-27 21:15:01 -10:00
home-paths.ts
…
index.ts [codex] feat(watchdog): add task watchdog control plane (#8339) 2026-06-19 15:38:52 -05:00
instrumentation.ts feat(server): opt-in OpenTelemetry auto-instrumentation (#3735) 2026-06-12 10:44:22 -07:00
log-redaction.ts
…
paths.ts
…
redaction.ts [codex] Roll up May 17 branch changes (#6210) 2026-05-17 17:15:06 -05:00
runtime-api.ts
…
startup-banner.ts
…
static-index-html.ts [codex] Bundle local branch fixes from PAP-10032 (#6604) 2026-05-25 07:25:26 -05:00
telemetry.ts
…
ui-branding.ts
…
version.ts
…
vite-html-renderer.ts
…
worktree-config.ts [codex] Bundle local branch fixes from PAP-10032 (#6604) 2026-05-25 07:25:26 -05:00