Skip to content

fix: yield slots during external waits - #1195

Open
allyblockcast[bot] wants to merge 6 commits into
masterfrom
pen-2074/release-external-wait-slots
Open

fix: yield slots during external waits#1195
allyblockcast[bot] wants to merge 6 commits into
masterfrom
pen-2074/release-external-wait-slots

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Aug 9, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source control plane people use to manage AI agents for work.
  • Its heartbeat scheduler owns execution slots, issue-bound queued runs, and durable monitor wake paths.
  • External CI and review waits were represented as active executions, so a completed code run could retain a slot while queued assignments starved.
  • Existing stale-run hardening bounded the leak but did not make yielding a platform lifecycle guarantee.
  • This pull request persists the external wait, terminates the active execution, releases its runtime reservation, and keeps the monitor/event path responsible for resumption.
  • The benefit is predictable slot availability without duplicate heartbeats or lost review work.

Linked Issues or Issue Description

What Changed

  • Detect pending external-service monitor state during terminal run cancellation and preserve it as the durable continuation owner.
  • Release external runtime reservations and issue execution locks when a run yields to that monitor.
  • Reconcile delayed Job deletion, deferred wakes, stale-lock recovery, and queued issue dispatch without creating duplicate heartbeats.
  • Add lifecycle, execution-policy route, stale-lock recovery, and context-summary coverage.
  • Reconcile the implementation with current master, including detached queued-run recovery and the explicit issue_assigned auto-checkout contract.

Verification

  • pnpm exec vitest run server/src/__tests__/heartbeat-process-recovery.test.ts server/src/__tests__/issue-execution-policy-routes.test.ts server/src/__tests__/recovery-stale-issue-lock-sweep.test.ts server/src/__tests__/heartbeat-context-summary.test.ts (335/335 passed after merging current master).
  • pnpm --filter @paperclipai/server typecheck passed.
  • pnpm --filter @paperclipai/server build passed.
  • git diff --check passed.

Risks

  • Behavioral risk is concentrated in heartbeat cancellation, external-runtime reservation reconciliation, issue execution locks, deferred wakes, and stale-lock recovery.
  • An incorrect release decision could dispatch competing work or suppress a required continuation; coverage pins both delayed deletion and terminal monitor resumption paths.
  • No schema migration or destructive infrastructure operation is included.
  • Rollback is a revert of 31bb91c21 and merge-resolution commit 49a636e07.

For core feature work, check ROADMAP.md first and discuss it in #dev before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See CONTRIBUTING.md.

Model Used

  • OpenAI openai/gpt-5.6-sol, reasoning-capable coding model with tool use and local code execution.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots
  • I have updated relevant documentation to reflect my changes
  • 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
  • I will address all Greptile and reviewer comments before requesting merge

Signed-off-by: Search <search@example.com>
@allyblockcast

allyblockcast Bot commented Aug 9, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: PEN-2074

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 9, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: PEN-2074

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 31bb91c

Looks good. I found no high-confidence correctness, error-handling, or lifecycle regressions in the external-wait yield path.

Critical Issues (0)

Important Issues (0)

Strengths

  • The cancellation path persists terminal state before external teardown and retains the issue lock until the exact runtime reservation is released.
  • The stale-lock and deferred-wake paths distinguish external-service resumption signals from ordinary activity, with focused regression coverage.

Recommended Action

  1. Safe to merge after the repository's required checks pass.

@allyblockcast

allyblockcast Bot commented Aug 9, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Verification
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

Signed-off-by: Search <search@example.com>
@allyblockcast

allyblockcast Bot commented Aug 9, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Verification
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 49a636e

Looks good. I found no high-confidence correctness, error-handling, lifecycle, or type-design regressions in the external-wait yield and recovery paths.

Critical Issues (0)

Important Issues (0)

Strengths

  • The route persists the monitor before yielding, while the cancellation path keeps the execution lock until the external reservation is safely released.
  • The deferred-wake and stale-lock paths distinguish external-service resume signals from ordinary activity, with targeted regression coverage.

Recommended Action

  1. Safe to merge after the repository required checks pass.

Signed-off-by: Search <search@example.com>

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 826ffdb

Critical Issues (0)

Important Issues (1)

  • [native-codex] server/src/services/heartbeat.ts:510 — A github_pr_synchronized wake is not considered an external-wait resume signal, so a push to the monitored PR is written as issue_external_wait_wake_suppressed at server/src/services/heartbeat.ts:25373 instead of waking the waiting issue. This leaves the issue idle until its timer fires even though its GitHub state changed and the existing review scheduling code explicitly treats synchronize as a fresh-head event (server/src/services/heartbeat.ts:7984).
    • Include github_pr_synchronized in the external-wait resume reasons and cover the resulting wake path with a focused test.

Suggestions (0)

Strengths

  • The cancellation path persists the yield before terminating an external runtime and holds the issue lock until the exact runtime reservation is safely released.
  • The added tests cover delayed Job deletion, deferred-wake retirement, and terminal-lock cleanup races.

Recommended Action

  1. Address the Important issue this cycle.

Signed-off-by: Search <search@example.com>
@allyblockcast

allyblockcast Bot commented Aug 10, 2026

Copy link
Copy Markdown
Author

Addressed the github_pr_synchronized external-wait resume finding in fd8eeb506. Both heartbeat and recovery resume classifications now include synchronized PR events, and the lifecycle regression test proves ordinary comments stay suppressed while a PR push resumes the waiting issue. Focused test: 1 passed; server typecheck passed.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: fd8eeb5

Looks good. I found no high-confidence correctness, error-handling, lifecycle, or type-design regressions in the external-wait yield and recovery paths.

Prior Findings Dispositioned (1)

  • prior:826ffdb important 1 — fixed — server/src/services/heartbeat.ts:780github_pr_synchronized is now an external-wait resume signal; the current-head lifecycle regression test exercises its resulting wake at server/src/__tests__/heartbeat-process-recovery.test.ts:5626.

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • Yielding persists the external monitor before cancelling and keeps the issue lock until the matching external reservation is released.
  • Recovery prevents ordinary deferred wakes from bypassing the persisted external monitor, while allowing the relevant GitHub and timer resumption signals.

Recommended Action

  1. Safe to merge after the repository required checks pass.

…ternal-wait-slots

Signed-off-by: Search <search@example.com>

# Conflicts:
#	server/src/services/heartbeat.ts
@allyblockcast
allyblockcast Bot requested a review from allyblockcast August 19, 2026 06:15

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 5797d3c

Looks good. The external-wait yield path, the terminal-cleanup-pending gating in the wake path, and the reservation-aware lock-release predicates all hold up. I found no high-confidence correctness, lifecycle, or type-design regressions.

Critical Issues (0)

Important Issues (0)

Suggestions (4)

  • [native-codex] server/src/services/heartbeat.ts:18602 — In the reservation reconciler's new terminal-run cleanup block, startNextQueuedRunForAgent(run.agentId) is the only unguarded await, while the two sibling calls immediately above it (releaseIssueExecutionAndPromote, finalizeAgentStatus) are explicitly .catch()-wrapped. Because this sits inside for (const { reservation, run } of pending), a throw here aborts the sweep and leaves the remaining pending reservations unreconciled. This matches the unguarded house style used elsewhere, so it may be deliberate — but the local asymmetry inside a batch loop suggests wrapping it in the same defensive .catch() as its neighbours.
  • [gstack/review] server/src/services/recovery/service.ts:241EXTERNAL_WAIT_RESUME_WAKE_REASONS is defined twice: here as a hardcoded 8-entry literal, and at server/src/services/heartbeat.ts:561 as GITHUB_STATE_CHANGE_WAKE_REASONS spread plus 5 extras. The two sets are identical today, but they must stay in lockstep or a resume signal will wake one path and be suppressed by the other. Since heartbeat.ts already re-exports from ./recovery/service.js, recovery/service.ts can own the constant and heartbeat.ts can import it.
  • [pr-review-toolkit/code] server/src/services/issues.ts:6524if (run && !TERMINAL_HEARTBEAT_RUN_STATUSES.has(run.status)) return false; is immediately followed by if (run && !await isTerminalOrMissingHeartbeatRun(...)) return false;, and the callee re-reads the run row and re-applies the same terminal check at server/src/services/issues.ts:6024. The first line is fully subsumed by the second and costs an extra round trip. Same pattern at server/src/services/issues.ts:6720 and server/src/services/issues.ts:6732.
  • [pr-review-toolkit/types] server/src/services/issues.ts:6017isTerminalOrMissingHeartbeatRun now also requires that no unreleased externalRuntimeReservations row exists, so the name no longer describes what it returns; a caller reading only the signature would expect a pure status predicate. Something like isReleasedTerminalOrMissingHeartbeatRun would keep the four call sites honest.

Strengths

  • The cancellation path is now durable-first: status is written via setRunStatusIfCurrentStatus CAS before process termination, with a bounded re-entry when the CAS loses to a concurrent transition, so a lost race no longer silently drops the cancellation.
  • repairTerminalRelease makes the yield idempotent — re-invoking cancelRun on an already-cancelled run with the same errorCode re-runs cleanup, and persistCancellationArtifacts dedupes the lifecycle event rather than appending a duplicate.
  • The reservation-aware predicates are applied consistently across all three lock-release surfaces (issue service, recovery sweep, wake path), so a terminal run holding an unreleased runtime slot can no longer have its issue lock stolen mid-teardown.
  • terminalRunCleanupPending correctly keeps activeExecutionRun non-null and forces deferral instead of admitting a new run while teardown is in flight, with the reconciler as the backstop if cleanup stalls.

Recommended Action

  1. No Critical or Important issues — safe to merge once the repository required checks pass.
  2. Consider the Suggestions opportunistically; the duplicated wake-reason set is the one most likely to bite later.

…ble (PEN-2383)

cancelRunInternal's cleanupExternalRuntime pre-checks the active reservation
and returns early, so it never reached deleteExactExternalRuntimeJob, which
owned the debug log for the benign no-reservation path. The code was intact
but unreachable, silencing the signal heartbeat-external-runtime-retry
asserts on.

Extract the log into logNoActiveReservationSkip and emit it from both the
early-return and the delete-refusal site, so skipping the pointless delete
call does not also skip the signal.

Signed-off-by: Search <search@example.com>
@github-actions

Copy link
Copy Markdown

@ally head 4338f17 has been awaiting review for 102.3h with no review on either surface (pulls/1195/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head 4338f17.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 4338f17

Looks good. The external-wait yield, durable cancellation, reservation-aware lock release, deferred-wake suppression, and stale-lock recovery changes are covered by focused tests. I found no high-confidence correctness, error-handling, lifecycle, or type-design regressions in the current head.

Critical Issues (0)

Important Issues (0)

Suggestions (1)

  • [native-codex] server/src/services/heartbeat.ts:18593 — The reservation reconciler catches issue-lock cleanup and agent finalization failures but leaves startNextQueuedRunForAgent as an unguarded await inside its batch loop. Consider isolating that dispatch failure so one agent's queue-start error cannot abort reconciliation of later pending reservations.

Strengths

  • The cancellation path persists terminal state before external teardown and retains the issue lock until the matching runtime reservation is released.
  • External-wait resume reasons are carried through coalescing, deferred-wake recovery, and normal wake dispatch, including synchronized PR pushes.
  • Focused lifecycle and recovery tests cover delayed Job deletion, deferred-wake retirement, stale-lock protection, and queued-run promotion.

Recommended Action

  1. Safe to merge after the repository's required checks pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants