Files
PaperClipAI/server
DottaandPaperclip 2a5f65c9ae refactor(server): extract heartbeat retry scheduling (#15617)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Heartbeat orchestration starts runs and recovers from supported
failures.
> - The heartbeat service still contains more than 22,000 lines after
earlier extractions.
> - Retry scheduling has a clear boundary, but it shares lifecycle
callbacks with the service.
> - This pull request moves retry scheduling into one module with
explicit dependencies.
> - It preserves the scheduling rules, database writes, and lock order.
> - The benefit is a smaller service and focused tests for the retry
boundary.

## Linked Issues or Issue Description

**Current behavior**

`server/src/services/heartbeat.ts` owns retry schedules, contention
deferrals, due promotion, and retry-now requests. These operations sit
among execution and recovery code.

**Proposed behavior**

Move these operations into `server/src/services/heartbeat/retries.ts`.
Keep the public heartbeat API and existing helper exports. The service
supplies its lifecycle callbacks and worktree cutoff.

**Reason and benefit**

Extract one complete area in a separate PR. This removes 1,641 lines
from the main service without changing retry policy. Future retry
changes can use a smaller module and direct tests.

**Breaking changes**

None. Existing imports, error-class identity, retry metadata, and API
outcomes remain available.

**Additional context**

Refs #15601, the preceding run-state extraction. Related open retry
changes: #14930, #13975, #15157, #12587, and #13276. This PR moves the
existing implementation. Those policy changes remain separate.

## What Changed

- Add `createHeartbeatRetries(db, dependencies)` for bounded retries,
workspace and connection deferrals, shared-workspace holder checks, due
promotion, and retry-now requests.
- Keep status transitions, event publication, issue-lock release,
plan-resume reporting, and worktree settings in the heartbeat service.
- Preserve all 140 existing public exports. Re-export the same helpers,
constants, and workspace-busy error class.
- Add 14 tests for construction, callback order, cancellation races,
concurrent scheduling, committed promotion, and retry-now scope.
- Document the module boundary in `doc/DEVELOPING.md`.

## Verification

- Existing focused suite before the move: 84 tests passed.
- Expanded focused suite after the move: 243 tests passed across 11
files. Command:
`pnpm exec vitest run server/src/services/heartbeat/retries.test.ts
server/src/__tests__/heartbeat-retry-scheduling.test.ts
server/src/__tests__/heartbeat-workspace-busy.test.ts
server/src/__tests__/heartbeat-model-fallback-warning.test.ts
server/src/__tests__/heartbeat-running-followup.test.ts
server/src/__tests__/heartbeat-issue-rewake-throttle.test.ts
server/src/__tests__/issue-scheduled-retry-routes.test.ts
server/src/modules/run-dispatch`
- After rebasing onto the latest master, 61 retry and plugin-idle tests
passed across four files.
- `pnpm -r typecheck` passed again on the rebased branch.
- `pnpm build` passed again on the rebased branch.
- `pnpm test:run` was started locally. The long serial run was stopped
after full CI passed; it is not claimed as a complete local pass.
- Full CI passed on commit `69e76f3eec75b0703570f1fce37ebfe9c63fa3ac`:
54 successful checks, two intentional Storybook skips, zero failures.
This includes general tests, serialized server tests, browser tests,
runner checks, typecheck, build, and canary verification.
- The setup-timeout review fix passed all 14 module tests and the server
typecheck.
- Greptile completed a fresh review of commit
`69e76f3eec75b0703570f1fce37ebfe9c63fa3ac`: 5/5, no open findings.
- A structural comparison confirmed 22 unchanged moved functions and one
unchanged three-line string reader, 16 unchanged declarations, one
unchanged API body, and all 140 preserved exports. The remaining service
body is unchanged after accounting for the new binding and removed
declarations.

## Risks

- A missing lifecycle callback or an eager call during construction
could change retry behavior. The dependency contract and direct tests
cover these boundaries.
- Concurrent scheduling must keep the issue-first lock order and reuse
one successor. The original transaction bodies are unchanged. PostgreSQL
tests cover concurrent calls from separate factory instances.
- Promotion must publish after commit and preserve worktree cutoffs.
Direct tests cover both paths.
- No schema, route, adapter, or retry-policy changes are included.

## Model Used

OpenAI GPT-6 via Codex. The exact serving model ID and context window
were not exposed in this session. Used reasoning, repository inspection,
shell tools, and code execution.

## 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)
- [x] 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] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-08 15:38:48 -05:00
..