Skip to content

fix(productivity-review): grace window for monitor lapse-to-service gap (BLO-21003) - #985

Merged
allyblockcast merged 22 commits into
masterfrom
codex/blo-21003-monitor-lapse-grace
Aug 4, 2026
Merged

fix(productivity-review): grace window for monitor lapse-to-service gap (BLO-21003)#985
allyblockcast merged 22 commits into
masterfrom
codex/blo-21003-monitor-lapse-grace

Conversation

@kkroo

@kkroo kkroo commented Aug 3, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work.
  • Productivity review watches long-running agent issues and opens review work when an issue appears unattended.
  • Some source issues intentionally park themselves behind a future or actively serviced monitor wake, so those should not count as abandoned while the monitor path can still reasonably service them.
  • BLO-19772 showed that a due monitor can sit in the scheduler/dispatch gap before the wake executes, producing a false long_active_duration review.
  • Ally found that a fixed grace was unsafe for open-review closure and too narrow for configured cadence plus the 50-monitor dispatch cap.
  • This replacement PR carries the fixed fix(productivity-review): grace window for monitor lapse-to-service gap (BLO-21003) #980 head under an independent author so Ally can provide a formal review.

Linked Issues or Issue Description

Paperclip issue: BLO-21003
Related incident: BLO-19772
Supersedes app-authored PR: #980

What happened?

deliberateFutureMonitor stopped suppressing long_active_duration reviews the instant monitorNextCheckAt passed now, even when the wake had not yet had time to be serviced. A fixed grace still missed large due-monitor backlogs because tickDueIssueMonitors only claims 50 monitors per pass.

Expected behavior

A monitor that lapsed only by normal scheduler, dispatch-claim, or bounded backlog latency should still suppress creation of a stale-review escalation. An already-open review should not be auto-closed during that due-but-unserviced gap because closing it creates a resolved-review snooze before dispatch succeeds.

Steps to reproduce

  1. Create an active source issue with monitorNextCheckAt just before now.
  2. Add at least 50 earlier due monitor rows so the source issue can sit behind the monitor dispatch batch.
  3. Run the productivity review pass before the monitor wake has executed.
  4. Observe that old behavior can open a false long_active_duration review for a monitored issue.

Paperclip version or commit

PR head 521f486be9c86710ef5c84065e02232860b40194 against Paperclip master.

Deployment mode

Self-hosted Paperclip server / productivity review service.

What Changed

  • Keeps open-review closure strict: existing reviews only auto-close for monitors whose monitorNextCheckAt is still in the future.
  • Derives new-review suppression from scheduler cadence plus the monitor dispatch claim TTL.
  • Treats a fresh monitorWakeRequestedAt claim as durable evidence that a lapsed monitor wake is still being serviced.
  • Accounts for due-monitor backlog position behind the 50-row dispatch cap when deciding whether lapse is still service latency.
  • Keeps genuinely long-lapsed, unclaimed monitor cases eligible for long_active_duration review.
  • Adds focused coverage for scheduler-derived grace, fresh claims, close-path protection, long-lapsed negative control, and the 50-row backlog case.

Verification

  • git diff --check passed.
  • pnpm --filter @paperclipai/server typecheck passed.
  • pnpm exec vitest run server/src/__tests__/productivity-review-service.test.ts loaded successfully but skipped locally because embedded Postgres support is unavailable in this host environment; CI should execute the embedded suite on the runner.

Risks

Low to moderate. The new-review suppression window intentionally delays review creation while the monitor scheduler can still plausibly service the due wake. The close path is conservative and no longer resolves already-open reviews during a due-but-unserviced gap.

Model Used

OpenAI Codex GPT-5 implemented the final review fixes, tests, typecheck, and independent-author handoff.

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

@allyblockcast

allyblockcast Bot commented Aug 3, 2026

Copy link
Copy Markdown

🔗 Paperclip issue: BLO-21003
🔗 Paperclip issue: BLO-19772

@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally please review exact head 521f486be9c86710ef5c84065e02232860b40194. This is the independent-author replacement for app-authored #980 and carries the backlog-grace fix for BLO-21003.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: 521f486

Important Issues (3)

  • [code/tests] server/src/services/productivity-review.ts:1068 — The backlog rank does not match the dispatcher's ordering for equal due times. This query counts every monitor whose monitorNextCheckAt is less than or equal to the source timestamp, while tickDueIssueMonitors orders ties by updatedAt. A large equal-time cohort can therefore make even the first monitor look many batches deep and suppress a genuine productivity review far beyond its service position. Recommendation: rank only rows preceding the source under the exact scheduler ordering, add a deterministic final tie-breaker to both queries, and cover an equal-timestamp cohort where the earliest row receives first-batch grace.
  • [gstack/native-codex] server/src/services/productivity-review.ts:1081 — The derived grace uses the remaining queue depth but is compared with total age since the monitor became due. As earlier batches drain, duePosition and grace shrink while dueAgeMs grows, so a monitor still legitimately queued can become reviewable before its dispatch tick. For example, after nine minutes of a ten-batch backlog, one remaining batch yields roughly six minutes of grace and the still-pending monitor is flagged. Recommendation: compute an absolute expected service deadline or preserve elapsed queue delay when converting remaining position into grace, then test reconciliation after multiple partial batches have drained.
  • [gstack/native-codex] server/src/services/productivity-review.ts:1073 — A concurrent scheduler claim can still produce the false review this change targets. Candidates are read earlier; if dispatch sets monitorWakeRequestedAt after that snapshot, this count excludes the now-fresh source, but the second suppression check at server/src/services/productivity-review.ts:1267 still evaluates the stale candidate with a null claim. Once base grace has elapsed and no earlier rows remain, backlog grace is zero and reconciliation creates a review despite the active claim. Recommendation: re-read the source monitor state as part of the decision, or return and evaluate its current durable claim atomically; add a claim-between-candidate-read-and-backlog-count regression test.

Strengths

  • The close path correctly remains stricter than new-review suppression, preventing a due-but-unserviced wake from starting the resolved-review snooze.
  • Fresh claim evidence is included in suppression activity, and the long-lapsed negative control preserves genuine escalation behavior.
  • The scheduler cadence, capped backlog, and sub-minute evidence formatting all receive focused regression coverage.

Recommended Action

  1. Fix the three Important issues before merge.
  2. Add equal-timestamp, partially drained backlog, and concurrent-claim regression cases.
  3. Re-run the focused embedded-Postgres suite and required CI checks.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally please review exact head 262b5b43d6914feec0bc40cb981b8acf2bd4e0a5. Addressed the three Important findings: monitor backlog rank now matches scheduler ordering including an id tie-breaker, backlog grace preserves elapsed queue delay while live predecessors remain, and long-active monitor suppression re-reads the source so fresh scheduler claims made after candidate selection are honored. Added focused regression coverage for equal due timestamps, partially drained backlogs, and claim-after-candidate-selection. Local verification: pnpm --filter @paperclipai/server typecheck passed; embedded-Postgres Vitest targets for productivity review and monitor scheduler were skipped on this host.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: 262b5b4

Prior Findings Dispositioned (3)

  • prior:521f486 important 1 — fixed — server/src/services/productivity-review.ts:1087 — The backlog query now uses the scheduler's (monitorNextCheckAt, updatedAt, id) ordering, including a deterministic id tie-breaker that is also present in tickDueIssueMonitors.
  • prior:521f486 important 2 — fixed — server/src/services/productivity-review.ts:1113 — Backlog grace now includes the already elapsed due age before adding remaining dispatch time, so draining earlier batches no longer shrinks the deadline below elapsed queue time.
  • prior:521f486 important 3 — still-present — server/src/services/productivity-review.ts:1125 — The source is re-read before backlog calculation, but a scheduler claim can still land after this read. The backlog query then excludes the freshly claimed source while the final suppression check reuses this stale, unclaimed row.

Important Issues (2)

  • [prior:521f486 important 3] server/src/services/productivity-review.ts:1125 — A scheduler claim racing between getCurrentIssue() and monitorBacklogGraceMs() can still create the false review this PR targets. If base grace has elapsed, the query excludes the now-claimed source, returns position zero, and deliberatePendingMonitor evaluates the stale row with no claim. Recommendation: make the current-state read and suppression decision atomic, or re-read the source after backlog calculation before returning an unsuppressed result; add a regression hook in this exact window.
  • [gstack/native-codex] server/src/services/productivity-review.ts:1102 — Freshly claimed predecessor monitors disappear from duePosition. When the source is immediately behind one in-flight predecessor, only the source remains counted, duePosition === 1 disables backlog grace, and an expired base grace can file a review while the scheduler is still dispatching the preceding row. Recommendation: count fresh-claimed predecessors that sort before the source as live queue work, while handling the source's own fresh claim separately; add a test with one claimed predecessor and an old due source.

Strengths

  • The backlog rank now matches scheduler ordering, including deterministic equal-time ties.
  • The elapsed queue delay is preserved as earlier batches drain.
  • The focused tests cover equal timestamps, partial backlog drainage, and a claim made before evidence collection.

Recommended Action

  1. Close both remaining claim-window races before merge.
  2. Add focused regressions for a source claim after the current-state read and for an in-flight predecessor.
  3. Run the embedded-Postgres targets in CI; the current review and security-review checks are green while policy remains pending.

@allyblockcast

allyblockcast Bot commented Aug 3, 2026

Copy link
Copy Markdown

Supplementary pass — a concurrent Ally run already submitted the operative verdict for 262b5b43d6914feec0bc40cb981b8acf2bd4e0a5. This non-duplicate finding is posted as a comment, not a second review.

Ally — Additional Finding

Reviewed head: 262b5b4

Important Issues (1)

  • [code-tests] server/src/services/productivity-review.ts:278 — Claim freshness uses requestedAt > now - TTL, while the dispatcher only permits reclaim when requestedAt < now - TTL. At exact equality the claim is neither fresh enough to suppress nor stale enough to dispatch, allowing a productivity review for a wake the scheduler still treats as claimed. Use matching boundary semantics and add an exact-TTL regression.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally please review exact head 5bdb03282d26dccfd6cb48bdbad730bb1b6750f0. Addressed the two remaining Important findings from the 262b5b4 review: after backlog calculation the suppression path re-reads the source and honors a fresh claim, and backlog ranking now counts fresh-claimed predecessor monitors as live queue work while handling the source claim separately. Added focused regressions for claim-after-current-state-read and fresh-claimed predecessor. Local verification: pnpm --filter @paperclipai/server typecheck, git diff --check, and focused productivity-review Vitest invocation attempted but skipped embedded-Postgres tests on this host.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: 5bdb032

Prior Findings Dispositioned (2)

  • prior:521f486 important 3 — fixed — server/src/services/productivity-review.ts:1140 — The source is now re-read after backlog calculation, and latestDirect at line 1143 honors a claim that landed after the initial current-state read.
  • prior:262b5b4 important 2 — still-present — server/src/services/productivity-review.ts:1083 — Fresh-claimed equal-time predecessors are compared using their current updatedAt, but the scheduler rewrites that field when claiming them, so production claims can move predecessors behind the unclaimed source and remove their queue contribution.

Important Issues (3)

  • [prior:262b5b4 important 2] server/src/services/productivity-review.ts:1083 — The claimed-predecessor fix does not preserve scheduler order for equal due timestamps. tickDueIssueMonitors sets a claimed row's updatedAt to now, so each of the 50 selected predecessors can stop satisfying updatedAt < source.updatedAt while that batch is still dispatching. A source just beyond the batch boundary then appears first, loses backlog grace, and can receive a false productivity review. The regression at server/src/__tests__/productivity-review-service.test.ts:1200 seeds a fresh claim but leaves updatedAt artificially older than the source, so it cannot expose the production race. Preserve the pre-claim ordering key, or rank claimed predecessors without relying on the mutated updatedAt, and make the test claim through the production update shape.
  • [gstack/native-codex] server/src/services/productivity-review.ts:1124 — Backlog grace is renewed indefinitely because every reconciliation returns dueAgeMs + remainingTime. Whenever any predecessor remains, dueAgeMs is therefore always below the newly computed grace, even if a poison predecessor has failed dispatch for hours or days. This contradicts the bounded service-latency goal and hides genuinely unserviced sources forever. Anchor the deadline to stable queue evidence rather than the current time, and add a negative control with a non-draining predecessor.
  • [code/tests] server/src/services/productivity-review.ts:1140 — The final monitor re-read is still separated from review creation by snooze/backoff/owner queries and evaluateSourceReviewability, which does not read monitor fields. A scheduler claim or rearm after this read but before createOrUpdateReview still creates the false review the durable-claim guard is intended to prevent. Revalidate monitor suppression in the creation critical section or make the decision conditional on unchanged monitor state; the existing beforeCreateOrUpdateReview hook can cover this final race window.

Strengths

  • The post-backlog source re-read closes the specific claim-after-current-state-read window from the prior review.
  • The open-review close path remains strict, avoiding a resolved-review snooze before monitor dispatch succeeds.
  • Scheduler ordering now has a deterministic id tie-breaker, and the focused suite covers the main lapse, backlog-drain, and direct-claim paths.

Recommended Action

  1. Preserve equal-time predecessor rank across scheduler claims.
  2. Bound backlog suppression to a stable service deadline.
  3. Make the final suppression decision race-safe with review creation and add the corresponding regressions.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally please review exact head 0c7aee0e61ce730ab1e042d6478dad85f19c4efe. This adds the missing exact-TTL boundary fix from the supplemental finding: monitor wake claims are now fresh at requestedAt >= now - ISSUE_MONITOR_WAKE_CLAIM_TTL_MS, matching the dispatcher reclaim boundary that only treats claims older than the threshold as stale. Added a focused regression for a lapsed monitor claimed exactly at the scheduler claim TTL boundary. Local verification: pnpm --filter @paperclipai/server typecheck, git diff --check, and focused productivity-review Vitest invocation attempted but skipped embedded-Postgres tests on this host. Please report whether any Critical or Important issue remains at this exact SHA.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally please review exact head 44c888ac16231dbce574ca47a96dc6a7d109b71e. This addresses the three remaining monitor-suppression findings from the prior pass: equal-time fresh-claimed predecessors are still counted after the scheduler mutates updatedAt, backlog grace is now bounded to a stable dispatch/claim deadline instead of recalculating from current due age forever, and review creation revalidates long-active monitor suppression after the final pre-create hook before writing a review. Also includes the exact-TTL boundary fix from the supplemental finding. Local verification: pnpm --filter @paperclipai/server typecheck, git diff --check, and focused productivity-review Vitest invocation attempted but skipped embedded-Postgres tests on this host. Please report whether any Critical or Important issue remains at this exact SHA.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: 44c888a

Prior Findings Dispositioned (3)

  • prior:262b5b4 important 2 — fixed — server/src/services/productivity-review.ts:1111 — Fresh-claimed equal-time monitors are now retained in queue state independently of their claim-mutated updatedAt, so the production scheduler update no longer drops an in-flight predecessor from backlog grace.
  • prior:5bdb032 important 2 — still-present — server/src/services/productivity-review.ts:1137 — The static dispatch deadline is anchored to the due time, but latestFreshPredecessorClaimedAt is recomputed on every pass; a failing predecessor reclaimed after each TTL moves freshPredecessorDeadlineMs forward again and can suppress the source indefinitely.
  • prior:5bdb032 important 3 — still-present — server/src/services/productivity-review.ts:1683 — Revalidation now happens after beforeCreateOrUpdateReview, but it remains a separate read before issuesSvc.create at line 1707; a monitor claim or rearm committed in that interval still permits creation of the false review.

Important Issues (2)

  • [prior:5bdb032 important 2] server/src/services/productivity-review.ts:1137 — Backlog suppression is still renewable rather than bounded to stable queue evidence. A predecessor whose dispatch repeatedly fails can be reclaimed every five minutes, and each new monitorWakeRequestedAt advances the derived deadline by another TTL. The negative control at server/src/__tests__/productivity-review-service.test.ts:1101 covers only an unclaimed, non-draining predecessor, not repeated production-shaped claims. Preserve an immutable service deadline for the source or cap claim-derived extension independently of later reclaims, then cover successive claims across multiple reconciliation passes.
  • [prior:5bdb032 important 3] server/src/services/productivity-review.ts:1683 — The final suppression check is not atomic with review creation. The new hook regression claims before this revalidation, but a scheduler claim or monitor rearm can still land after the check returns null and before issuesSvc.create writes at line 1707. Serialize the source-state check with creation or make creation conditional on unchanged monitor state, and add a test hook in the revalidation-to-create window.

Strengths

  • Equal-time claimed rows no longer disappear solely because the scheduler rewrites updatedAt, and the scheduler now has a deterministic id tie-breaker.
  • The direct claim boundary matches scheduler eligibility at exactly the five-minute TTL.
  • Open-review closure remains strict, avoiding a resolved-review snooze during a due-but-unserviced monitor gap.

Recommended Action

  1. Bound claim-derived backlog grace against immutable source/queue evidence.
  2. Make the final monitor suppression decision race-safe with review creation.
  3. Add production-shaped regressions for repeated predecessor reclaims and the revalidation-to-create interval.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: 14b18d8

Prior Findings Dispositioned (2)

  • prior:5bdb032 important 2 — fixed — server/src/services/productivity-review.ts:1167 — Fresh predecessor claims are now capped against a deadline derived from the original due time and queue position, so later reclaims cannot renew suppression indefinitely.
  • prior:5bdb032 important 3 — fixed — server/src/services/productivity-review.ts:1760 — The final monitor check now locks the source row inside the same issue-creation transaction and holds that lock through the review insert, serializing source claims and rearms against creation.

Important Issues (2)

  • [gstack/native-codex] server/src/services/issues.ts:8264 — The new abortable beforeInsert hook runs after allocateIdentifier() may create a Linear issue. Monitor suppression is an expected control-flow outcome that throws from this hook; PostgreSQL rolls back, but the external Linear issue then depends on best-effort deletion, whose failure is silently dropped. A normal monitor race can therefore leave a dangling external review issue. Run the guard before external identifier allocation while retaining its lock through insertion, or provide a pre-side-effect transactional hook for this use case, and add a Linear-provider suppression regression.
  • [gstack/native-codex] server/src/services/productivity-review.ts:1193 — The transactional final check locks only the source row; predecessor claim state is read without a coordinating lock. After this backlog query decides the absolute deadline has elapsed, a scheduler can commit a fresh claim for an earlier row before the review insert. That claim is durable evidence the queue is being serviced and can extend suppression within the bounded cap, but the stale query result still allows a false review. Coordinate scheduler claims and review creation with a shared lock or lock/revalidate the relevant queue state in a consistent order, and cover a predecessor claim in the backlog-query-to-insert window.

Strengths

  • Backlog grace is now bounded against stable due-time evidence and has a repeated-reclaim negative control.
  • Scheduler ordering is deterministic across monitorNextCheckAt, updatedAt, and id, including equal-time claimed predecessors.
  • Source monitor claims and rearms are revalidated under a row lock in the insertion transaction, closing the previously reported source-row race.

Recommended Action

  1. Move the suppression guard ahead of external identifier side effects.
  2. Serialize fresh predecessor claims with the final backlog decision.
  3. Add focused regressions for both interleavings and run the embedded-Postgres suite.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: f6379eb

Prior Findings Dispositioned (2)

  • prior:14b18d8 important 1 — fixed — server/src/services/issues.ts:8189 — The abortable guard now runs before allocateIdentifier(), so a monitor suppression cannot create a Linear-side identifier before the transaction aborts.
  • prior:14b18d8 important 2 — fixed — server/src/services/productivity-review.ts:1143 — The final transaction now locks the source and qualifying queue rows, then recomputes backlog state while those locks are held through the review insert, preventing a predecessor claim from committing between revalidation and creation.

Important Issues (1)

  • [gstack/native-codex] server/src/services/productivity-review.ts:1143 — The race fix takes an unbounded FOR UPDATE lock over every qualifying predecessor across all active companies and holds those locks through the rest of issuesSvc.create, including the documented Linear GraphQL call in allocateIdentifier(). A deep backlog plus a slow Linear request can therefore block the scheduler's first claim update and stall the entire sequential 50-monitor dispatch batch, as well as unrelated mutations to every locked issue. This directly prolongs the lapse-to-service gap the PR is intended to tolerate. Serialize the final decision with a short-lived advisory/queue lock or conditional insert rather than retaining a global predecessor row set across external side effects, and add a concurrency regression proving monitor claims are not blocked by review identifier allocation.

Strengths

  • The suppression guard now precedes external identifier side effects.
  • The bounded claim deadline prevents repeated predecessor reclaims from renewing grace forever.
  • The focused tests cover direct and predecessor claims, TTL boundaries, equal-time ordering, negative controls, and final-window races.

Recommended Action

  1. Shorten or replace the queue-wide locking critical section before merge.
  2. Add a concurrency regression covering a slow identifier provider alongside monitor dispatch.
  3. Run the embedded-Postgres suite and required CI checks.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally review exact head ec1400f179954c02f56c7144abf89cb2de22ac22. The follow-up replaces the final predecessor FOR UPDATE row-set lock with a transaction-scoped advisory monitor-queue lock, and monitor claims use tryLockIssueMonitorQueue so scheduler claims do not block behind slow review creation or identifier allocation. Added a concurrency regression for slow final review creation alongside monitor dispatch; local server typecheck and git diff --check passed, while embedded-Postgres Vitest skips on this host. Please report explicitly whether any Critical or Important issue remains at this exact SHA.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: ec1400f

Prior Findings Dispositioned (1)

  • prior:f6379eb important 1 — still-present — server/src/services/productivity-review.ts:1189 — The predecessor row locks were replaced with a global advisory lock, but that lock is still held through issuesSvc.create, including external identifier allocation. Scheduler claims use a non-blocking try-lock and therefore make no progress while creation is paused.

Important Issues (1)

  • [prior:f6379eb important 1] server/src/__tests__/productivity-review-service.test.ts:1675 — The replacement lock still stalls monitor service globally during review creation. The new regression calls tickDueIssueMonitors while the final create transaction is paused and explicitly expects triggered === 0; line 1681 also confirms the predecessor remains unclaimed. Thus a slow or hung Linear identifier request no longer blocks the scheduler query, but it still causes every due monitor batch across all companies to be skipped until the review transaction releases the global lock, recreating the lapse-to-service gap this PR is meant to bound. Keep the queue lock only around a short atomic decision/claim boundary, or use a conditional insert/immutable queue epoch that does not suppress scheduler progress across external side effects; update the concurrency test to require the due predecessor to be claimed while review creation is paused.

Strengths

  • The final source-state check runs before external identifier side effects and remains in the issue-creation transaction.
  • The scheduler uses a non-blocking lock attempt, so its loop returns promptly rather than waiting on a slow review transaction.
  • Backlog ordering, bounded claim grace, TTL boundaries, and long-lapsed negative controls have focused coverage.

Recommended Action

  1. Shorten the global queue critical section so monitor claims continue during identifier allocation.
  2. Change the concurrency regression to assert actual claim progress, not only a prompt zero-result return.
  3. Run the embedded-Postgres suite and required CI checks.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally review exact head fa66701955581fd1eb6c2940b44cfdc88834fb23. This fixes the ec1400f blocker by moving the final monitor-queue revalidation into its own short transaction-scoped advisory-lock boundary before issue creation, releasing the queue lock before identifier allocation and issue insert. The concurrency regression now pauses before final revalidation, proves heartbeat can claim a due predecessor while review creation is paused, and expects the review to suppress rather than create. Local verification: pnpm --filter @paperclipai/server typecheck passed, git diff --check passed, and the focused embedded-Postgres Vitest file still skips on this host. Please report explicitly whether any Critical or Important issue remains at this exact SHA.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally review exact head fa66701957000f398270d7f6459f7c6e9ccbf296. Correction: the prior comment had an incorrect full SHA expansion after the fa6670195 prefix. This is the verified GitHub head for the monitor queue-lock release fix. Please review this exact SHA and report whether any Critical or Important issue remains.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: fa66701

Prior Findings Dispositioned (1)

  • prior:f6379eb important 1 — fixed — server/src/services/productivity-review.ts:1748 — The advisory queue lock is now confined to the final revalidation transaction and is released before issuesSvc.create, so slow identifier allocation no longer makes scheduler claim attempts skip the entire due-monitor queue.

Important Issues (1)

  • [gstack/native-codex/tests] server/src/services/productivity-review.ts:1761 — Releasing the queue and source-row locks before issuesSvc.create reopens the final check-to-insert race. A scheduler claim, monitor rearm, or source-state change can commit after the transaction at line 1758 returns but before creation starts here, and the stale decision still creates and wakes a productivity review. The concurrency regression pauses in beforeFinalMonitorSuppressionRevalidation, so its claim lands before the protected read rather than in this post-commit window. Recommendation: establish a durable conditional review reservation/insert in the same short queue-locked transaction, then perform identifier allocation and other external side effects after that reservation; add a hook and regression between revalidation commit and create.

Strengths

  • The previous global-lock stall is removed, and the scheduler now demonstrates real claim progress while review reconciliation is paused.
  • Backlog ordering remains deterministic and claim-derived grace stays bounded against repeated predecessor reclaims.
  • The focused suite covers direct claims, predecessor claims, TTL boundaries, long-lapsed negative controls, and Linear side-effect ordering.

Recommended Action

  1. Close the post-revalidation check-to-insert race before merge.
  2. Add a regression that claims or rearms the monitor after the lock transaction commits and before issue creation.
  3. Run the embedded-Postgres suite and required CI checks.

Paperclip-Paperclip and others added 11 commits August 3, 2026 06:03
…ap (BLO-21003)

A long_active_duration review fired on an episode whose own evidence block
accounted 0m unattended, because deliberateFutureMonitor stopped suppressing
the instant monitorNextCheckAt passed `now`, with no allowance for the
dispatch pipeline (scheduler tick, K8s Job creation, pod scheduling) that
follows before the wake is actually serviced. Widen the boundary by a bounded
grace window, and stop flooring genuine sub-minute unattended residue to "0m"
in the evidence line so a near-boundary case can't misreport as measured-zero.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

Ally, please publish a fresh exact-head review for 2a6eaf2. The commitperclip review check has completed successfully on this head. The stale null-identifier/null-issueNumber reservation recovery finding from 6e859e6 is addressed with restart-safe finalization and regression coverage. Please state whether any Critical or Important issue remains at this exact SHA.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: 2a6eaf2

Prior Findings Dispositioned (1)

  • prior:6e859e6 important 1 — still-present — server/src/services/productivity-review.ts:754 — Stale reservations are retried, but Linear identifier allocation still performs a fresh external IssueCreate without durable recovery state. If Linear creates the issue and the worker dies before lines 764-788 commit its identifier/link, the local transaction rolls back and the next stale-recovery pass creates a second Linear issue; the in-process compensation at line 794 cannot run after process loss.

Important Issues (3)

  • [prior:6e859e6 important 1] server/src/services/productivity-review.ts:754 — Reservation recovery is not idempotent for Linear-backed companies. allocateIdentifier() calls Linear IssueCreate without a stable idempotency key (server/src/services/identifier-allocator.ts:322), while the returned external ID is only persisted later in the same local transaction. A crash or ambiguous HTTP response after Linear commits but before the local commit leaks the first Linear issue and recovery creates another. Persist a durable external-allocation intent/idempotency key before the call, or reconcile the reservation to the already-created Linear issue; add a process-loss/ambiguous-response regression for the Linear provider.
  • [code/gstack/native-codex] server/src/services/productivity-review.ts:1828 — Concurrent stale-recovery passes both emit creation side effects. They can both read the same stale reservation; the finalization advisory lock serializes them, but the second call returns the already-finalized row at line 749 and both callers then execute finishCreatedProductivityReview() at line 1834. This duplicates issue.productivity_review_created activity and assignment wakeups. Return whether this call actually finalized the reservation, or make the activity/wakeup path durably idempotent; add a two-reconciler stale-recovery test.
  • [gstack/native-codex] server/src/services/productivity-review.ts:1834 — Recovery commits identifier finalization before recording creation activity and enqueueing the assignment wake. If the worker dies after finalization but before finishCreatedProductivityReview(), later passes see a normal identified review at lines 1846-1854 and never retry the missing side effects; a failure after line 993 can similarly leave activity without a wake. Persist a finalization/outbox state and replay it idempotently so both activity and wake delivery survive process loss.

Strengths

  • The stale internal-provider reservation now receives an identifier instead of remaining permanently unactionable.
  • The reservation age guard avoids interfering with an ordinary in-flight finalization.
  • The focused recovery test verifies the basic internal identifier, activity, and wakeup path.

Recommended Action

  1. Make external Linear allocation recoverable without duplicate side issues.
  2. Make stale recovery and its activity/wakeup side effects idempotent under concurrency and process loss.
  3. Add Linear-provider, concurrent-recovery, and post-finalization restart regressions before merge.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

Ally, please re-review exact head 00cece6.

Follow-up for the three Important findings on 2a6eaf2:

  • Linear-backed stale reservation recovery now uses the reserved Paperclip review UUID as the Linear IssueCreate id/idempotency key, looks up that Linear issue before create, and recovers ambiguous create failures by looking it up again.
  • Reserved review finalization now returns whether the current caller actually finalized the row, so concurrent stale-recovery callers do not both count as creators.
  • Created activity and assignment wake side effects are replayable under a per-review advisory lock. The assignment wake uses a stable idempotency key and writes a durable activity marker after an existing or newly queued wake is known, so post-finalization/process-loss retries do not duplicate side effects.

Regression coverage added for Linear existing-id reuse, Linear create idempotency key submission, ambiguous Linear create recovery, concurrent stale reservation recovery, and post-finalization side-effect replay.

Local verification on this head:

  • pnpm --filter @paperclipai/server typecheck
  • pnpm exec vitest run server/src/tests/allocate-identifier-linear-happy.test.ts server/src/tests/productivity-review-service.test.ts --no-file-parallelism --maxWorkers=1 (both embedded-Postgres suites skipped on this host)
  • git diff --check

Please state whether any Critical or Important issue remains at this exact SHA.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: 00cece6

Prior Findings Dispositioned (3)

  • prior:6e859e6 important 1 — still-present — server/src/services/productivity-review.ts:805 — Linear creation now uses the reservation UUID, but an ambiguous create is recovered with only one immediate lookup; if that lookup errors or returns null after Linear committed, this catch deletes the local reservation at lines 809-812, so the next reconciliation allocates a new UUID and can create a duplicate Linear issue.
  • prior:2a6eaf2 important 2 — fixed — server/src/services/productivity-review.ts:758 — Finalization now returns finalized: false to the losing caller under the per-review advisory lock, and the separately locked side-effect replay lets only the caller that changed durable state report creation.
  • prior:2a6eaf2 important 3 — still-present — server/src/services/productivity-review.ts:1108 — The replay path awaits enqueueWakeup() but ignores its nullable result, sets wakeAlreadyProcessed = true unconditionally at line 1129, and writes the durable “wake enqueued” marker even when scheduling returned null without a queued/coalesced run.

Important Issues (2)

  • [prior:6e859e6 important 1] server/src/services/productivity-review.ts:805 — Linear-backed recovery still fails closed by deleting the idempotency anchor after an unconfirmed create. A timeout followed by a transient/replica-lagged IssueById miss reaches this catch with createdLinearIssueId === null; deleting the null-identifier reservation frees the active-review slot, and the next pass reserves a different UUID even though the first Linear issue may exist. Preserve the reservation on ambiguous external allocation failures and retry lookup/create with the same UUID until the outcome is known; add a regression where the post-create lookup initially fails or returns null and a later reconciliation finds the original Linear issue.
  • [prior:2a6eaf2 important 3] server/src/services/productivity-review.ts:1129 — A wake suppression is persisted as successful delivery. Production heartbeatService.enqueueWakeup() returns null for temporary scheduling suppression and other non-queued outcomes, but this path marks it processed and inserts issue.productivity_review_assignment_wake_enqueued; future reconciliations then skip the wake permanently. Only write the marker when the returned run/request proves a durable queued or coalesced wake exists, or confirm the idempotency key in agent_wakeup_requests after the call; add a regression where the first enqueue returns null and the next reconciliation retries successfully.

Strengths

  • The stable Linear issue ID supports lookup-before-create and recovery after a confirmed ambiguous response.
  • Per-review advisory locks and the finalized result prevent concurrent stale-recovery callers from duplicating the normal success path.
  • Side-effect replay has stable activity names and a deterministic wake idempotency key, providing the right durable primitives once unsuccessful enqueue outcomes are distinguished.

Recommended Action

  1. Retain the reservation across unconfirmed Linear allocation outcomes.
  2. Mark assignment wake completion only after durable enqueue evidence exists.
  3. Add the two failure/retry regressions and run the embedded-Postgres suites in CI.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

Ally, please re-review exact head 004aa34fcd373734b34314d1dc43fd5949518a57.

Follow-up for the two remaining Important findings on 00cece6:

  • Linear IssueCreate ambiguity now throws LinearIssueCreateUnconfirmedError; reserved productivity reviews preserve their null-identifier reservation on that error so the next reconciliation retries with the same review UUID/idempotency key instead of deleting the anchor and allocating a new UUID. Added a regression where post-create lookup initially returns null and a later retry finds the original Linear issue by the same key.
  • Assignment wake replay now treats enqueueWakeup() returning null as non-delivery unless a durable wake request with the idempotency key exists in a queued/claimed/coalesced/deferred state. It no longer writes issue.productivity_review_assignment_wake_enqueued on suppressed/null wake outcomes. Added a regression where the first enqueue returns null and the next reconciliation retries and marks only the successful wake.

Local verification on this head:

  • pnpm --filter @paperclipai/server typecheck
  • pnpm exec vitest run server/src/tests/allocate-identifier-linear-happy.test.ts server/src/tests/productivity-review-service.test.ts --no-file-parallelism --maxWorkers=1 (both embedded-Postgres suites skipped on this host)
  • git diff --check

Please state whether any Critical or Important issue remains at this exact SHA.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally please publish a fresh exact-head review for 004aa34. The commitperclip review check is green on this head. The two remaining findings from 00cece6 were addressed: unconfirmed Linear creates now preserve the reservation/idempotency key, and null enqueue results no longer write the wake-enqueued marker unless durable queued/coalesced request evidence exists. Please state whether any Critical or Important issue remains at this exact SHA.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: 004aa34

Prior Findings Dispositioned (2)

  • prior:6e859e6 important 1 — still-present — server/src/services/identifier-allocator.ts:173 — Retries preserve the reservation UUID after an unconfirmed create, but the retry's initial IssueById lookup is outside the ambiguity-conversion try; a transient lookup failure remains generic and causes finalizeReservedProductivityReviewIssue to delete the stable reservation at server/src/services/productivity-review.ts:822.
  • prior:2a6eaf2 important 3 — fixed — server/src/services/productivity-review.ts:1143 — A null enqueue result now requires a durable queued/claimed/coalesced/deferred request with the same idempotency key before the wake marker is written.

Important Issues (3)

  • [prior:6e859e6 important 1] server/src/services/identifier-allocator.ts:173 — The Linear idempotency anchor can still be lost during recovery. If an earlier IssueCreate committed ambiguously and the next reconciliation's preliminary IssueById call fails transiently, that error bypasses LinearIssueCreateUnconfirmedError; the finalizer then deletes the null-identifier reservation and a later pass allocates a new UUID, permitting a duplicate Linear issue. Convert failures of this preliminary lookup into the same unconfirmed outcome and retain the reservation; add a regression where the retry lookup errors before a later pass finds the original issue.
  • [gstack/native-codex] server/src/services/productivity-review.ts:1937 — Reservation recovery still depends on the source re-entering the normal unsuppressed candidate path. If the worker dies after committing a reservation and the source becomes terminal, gains a pending monitor, or otherwise stops producing evidence, this branch is never reached; the active null-identifier todo review remains unactionable and blocks replacement indefinitely. Recover stale reservations in an independent sweep or before source candidate/suppression filtering, with a regression where the source changes state after reservation.
  • [gstack/native-codex] server/src/services/productivity-review.ts:75completed wake requests are excluded from durable delivery evidence. If enqueue commits, the wake finishes, and the finishing transaction/process then fails before writing the activity marker, the next reconciliation treats the completed request as absent and enqueues the same assignment again despite the stable idempotency key. Count terminal successful delivery as processed, or make enqueue idempotency replay completed requests; add a process-loss regression between enqueue completion and marker commit.

Strengths

  • The reported null-enqueue path now avoids writing a false success marker and retries on the next reconciliation.
  • Linear create retries use the reservation UUID as IssueCreateInput.id, and confirmed delayed visibility reuses the original external issue.
  • Queue locking and review reservation establish a short linearization point without holding the monitor queue across external identifier allocation.

Recommended Action

  1. Preserve the Linear reservation across every unconfirmed lookup/create outcome.
  2. Make stale reservation recovery independent of source candidacy.
  3. Treat completed idempotent wakes as durable prior delivery and cover the process-loss window.

@allyblockcast

allyblockcast Bot commented Aug 3, 2026

Copy link
Copy Markdown

Supplementary pass: a concurrent Ally run already submitted the operative verdict for this exact head. This non-duplicate finding is posted as a comment, not a second review.

Ally — Additional Finding

Reviewed head: 004aa34

Critical Issues (1)

  • [code/gstack/native-codex] server/src/services/productivity-review.ts:1100 — The new finish path self-deadlocks with production wake enqueue. After inserting the created activity, this transaction updates the review row here and retains its row lock, then awaits deps.enqueueWakeup() at line 1122. Production enqueue opens another transaction and executes SELECT ... FOR UPDATE on that same review (server/src/services/heartbeat.ts:23883). The inner transaction waits for the outer row lock while the outer transaction waits for enqueue to return, so every newly created review can hang until timeout and retry the same cycle. Commit the issue/activity work before enqueueing, then persist the wake marker in a separate idempotent transaction; add an integration regression using the real heartbeat service rather than a mock enqueue function.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

Ally, please re-review exact head a37ff015db1d015946544287f175e9731bc330a7.\n\nChanges since 004aa34:\n- Linear stable-id preliminary lookup failures now throw LinearIssueCreateUnconfirmedError, preserving the reservation instead of deleting the idempotency anchor\n- stale reserved productivity-review rows are recovered in a pre-candidate sweep, so terminal/suppressed sources do not leave null-identifier reviews stuck\n- completed assignment wake requests count as durable wake delivery evidence before writing the wake marker\n- added regressions for retry lookup errors, stale reservation after source leaves candidates, and completed wake process-loss replay\n\nLocal checks:\n- pnpm --filter @paperclipai/server typecheck\n- pnpm exec vitest run server/src/tests/allocate-identifier-linear-happy.test.ts server/src/tests/productivity-review-service.test.ts --no-file-parallelism --maxWorkers=1 (embedded Postgres suites skipped on this host)\n- git diff --check

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: a37ff01

Prior Findings Dispositioned (3)

  • prior:6e859e6 important 1 — fixed — server/src/services/identifier-allocator.ts:174 — The preliminary stable-ID lookup is now wrapped, and lookup failures are converted to LinearIssueCreateUnconfirmedError, preserving the reservation for a later retry with the same UUID.
  • prior:004aa34 important 2 — fixed — server/src/services/productivity-review.ts:2246 — Stale null-identifier reservations are now recovered before source candidate filtering, so a source leaving the active candidate set no longer strands the reservation solely because normal evidence collection does not revisit it.
  • prior:004aa34 important 3 — fixed — server/src/services/productivity-review.ts:75 — Completed wake requests now count as durable delivery evidence, preventing replay from enqueueing a second assignment wake after completion won the race with marker persistence.

Critical Issues (1)

  • [code/gstack/native-codex] server/src/services/productivity-review.ts:1111 — The production assignment-wake path still self-deadlocks. finishCreatedProductivityReview() updates the review row inside its transaction, retains that row lock, and then awaits deps.enqueueWakeup() at line 1133. The real heartbeat enqueue opens another transaction and executes SELECT ... FOR UPDATE on the same review at server/src/services/heartbeat.ts:23883, so the inner transaction waits for the outer lock while the outer transaction waits for enqueue to return. Commit the issue/activity transaction before enqueueing with the stable idempotency key, then persist the wake marker in a separate short transaction (or use an outbox); add an integration regression using the real heartbeat service rather than a mock enqueue function.

Important Issues (2)

  • [code/tests/gstack] server/src/services/productivity-review.ts:2313 — Stale-reservation recovery finalizes and wakes every reservation without checking whether its source is now terminal or otherwise no longer reviewable. Because the close sweep runs before recovery, the new done-source test intentionally creates and wakes an obsolete review that the next reconciliation will close. Revalidate source reviewability before allocation; atomically retire terminal-source reservations without assigning an identifier or waking a manager, and change the regression to assert no wake.
  • [gstack/native-codex] server/src/services/productivity-review.ts:826 — A confirmed idempotent Linear recovery can still lose its anchor on a later local failure. When IssueById finds the existing Linear issue, createdLinearSideIssue is false, so a subsequent issue update, link insert, or commit failure reaches this generic catch with createdLinearIssueId === null and deletes the reservation at line 833. The external issue remains, but the next reconciliation allocates a new reservation UUID and can create a duplicate Linear issue. Preserve the reservation after any successful Linear lookup/allocation and retry local finalization with the same UUID; only delete when failure is proven to precede external allocation.

Strengths

  • Stable-ID lookup failures now retain the Linear idempotency anchor instead of falling through to a new reservation.
  • Reservation discovery is independent of active-source candidate filtering, and completed wake requests are recognized as durable prior delivery.
  • The monitor queue ordering, bounded grace, and short reservation linearization point have extensive focused regression coverage.

Recommended Action

  1. Remove the nested row-lock self-deadlock before merge.
  2. Retire obsolete terminal-source reservations without manager wakes.
  3. Preserve the Linear reservation across post-lookup local failures and run the embedded-Postgres integration targets.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

Ally, please re-review exact head 2e832fcf0b2d6662b067d819994b8083940dd712.\n\nAdditional follow-up since a37ff01 for the supplementary self-deadlock finding on 004aa34:\n- productivity review finish now claims/repairs created/wake state in short transactions only\n- assignment wake enqueue runs after the finish transaction commits, so production enqueue can take its own review row lock without deadlocking\n- a fresh wake-started activity fences concurrent finishers; null/throwing enqueue releases the claim with wake_failed so immediate retry still works\n- added a row-lock replay regression where enqueueWakeup opens a transaction and SELECTs the review FOR UPDATE\n\nIncludes the prior a37ff01 fixes for Linear unconfirmed lookup, stale reservation pre-candidate recovery, and completed wake durable evidence.\n\nLocal checks:\n- pnpm --filter @paperclipai/server typecheck\n- pnpm exec vitest run server/src/tests/allocate-identifier-linear-happy.test.ts server/src/tests/productivity-review-service.test.ts --no-file-parallelism --maxWorkers=1 (embedded Postgres suites skipped on this host)\n- git diff --check

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: 2e832fc

Prior Findings Dispositioned (3)

  • prior:a37ff01 critical 1 — fixed — server/src/services/productivity-review.ts:1377 — The wake claim transaction now commits before enqueueWakeup() is called at line 1388, so the real heartbeat service can lock the review row without waiting on its caller's retained row lock.
  • prior:a37ff01 important 1 — still-present — server/src/services/productivity-review.ts:2550 — Stale-reservation recovery still finalizes every reservation with an existing source and then wakes its assignee; it does not reject terminal or otherwise no-longer-reviewable sources before allocation. The current regression at server/src/__tests__/productivity-review-service.test.ts:2143 explicitly expects a review and wake after the source is already done.
  • prior:a37ff01 important 2 — still-present — server/src/services/productivity-review.ts:836 — A successful idempotent Linear lookup leaves createdLinearIssueId null, so any later local update/link/commit failure reaches this catch, deletes the stable null-identifier reservation at lines 839-842, and allows a later pass to allocate a new UUID for the already-existing external issue.

Important Issues (3)

  • [prior:a37ff01 important 1] server/src/services/productivity-review.ts:2550 — Recovery creates and assigns an obsolete productivity review when its source became terminal after reservation. Revalidate source reviewability before identifier allocation and atomically retire terminal-source reservations without emitting creation activity or an assignment wake; change the terminal-source regression to require no wake.
  • [prior:a37ff01 important 2] server/src/services/productivity-review.ts:836 — Confirmed Linear recovery can still lose its idempotency anchor after a local finalization failure. Preserve the reservation after any successful Linear lookup/allocation and retry the local update/link with the same UUID; delete only when failure is proven to precede external allocation.
  • [code/tests/gstack/native-codex] server/src/services/productivity-review.ts:1355 — Wake-claim lease time mixes two clocks. Fresh claims are stamped with the reconciliation-wide evidence.generatedAt at line 1374 but are considered active against Date.now(). A scan or prior work lasting five minutes therefore inserts an already-expired claim, allowing another reconciler to call enqueueWakeup concurrently. Retries that reuse the same generated time also leave started and failed rows tied, while the ordering at lines 1103-1111 always chooses failed ahead of the newer start and prevents successful completion from recording its marker. Stamp and compare claims using one transaction/database clock, preserve event order independently of evidence time, and add long-scan plus same-time failed-then-successful retry regressions.

Strengths

  • Moving the real enqueue outside the review-locking transaction resolves the previously reported self-deadlock.
  • The new advisory claim separates durable coordination from the external wake side effect and includes explicit failure/retry markers.
  • Existing coverage continues to exercise bounded monitor grace, queue ordering, reservation recovery, and assignment-wake replay.

Recommended Action

  1. Retire obsolete terminal-source reservations without finalization or wake delivery.
  2. Preserve the Linear reservation through post-lookup local failures.
  3. Use one authoritative clock and deterministic sequence for wake-claim leases and retries.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: b4dd226

Prior Findings Dispositioned (3)

  • prior:a37ff01 important 1 — still-present — server/src/services/productivity-review.ts:2633 — Recovery now retires terminal sources, but it does not apply the normal reviewability conditions before finalization; hidden, blocked, in-review, user-assigned, or reassigned sources still reach finalization at line 2649, while a disappeared source is left as a permanent active reservation at line 2630.
  • prior:a37ff01 important 2 — fixed — server/src/services/productivity-review.ts:810 — Any confirmed Linear allocation now sets preserveReservation before the local issue/link writes, so a later local failure retains the stable UUID anchor for retry.
  • prior:2e832fc important 3 — fixed — server/src/services/productivity-review.ts:1387 — Wake leases are now stamped and compared with the same database clock, and line 1404 adds a monotonic attempt sequence that disambiguates equal-time failed/retried claims.

Important Issues (1)

  • [prior:a37ff01 important 1] server/src/services/productivity-review.ts:2633 — Stale reservation recovery still does not revalidate whether the source remains reviewable. Only terminal status is handled; a source that became hidden, blocked, in_review, user-assigned, unassigned, or reassigned to another agent is finalized and wakes the reviewer selected from stale source ownership. If the source was deleted, line 2630 leaves the null-identifier reservation active forever. Apply the same status, visibility, and assignment checks as evaluateSourceReviewability() before allocation, then atomically retire every no-longer-reviewable or missing-source reservation without a creation activity or assignment wake. Add regressions for reassignment/non-candidate status and a missing source.

Strengths

  • Confirmed Linear lookup/allocation now preserves the reservation UUID through downstream local failures.
  • Assignment-wake claims use one authoritative clock and deterministic attempt sequencing, closing the long-scan and equal-time retry races.
  • Terminal-source recovery now retires the reservation without allocating an identifier or waking a manager.

Recommended Action

  1. Revalidate all source reviewability conditions during stale reservation recovery.
  2. Retire missing and otherwise no-longer-reviewable reservations without side effects.
  3. Add focused recovery regressions and run the embedded-Postgres suite.

@allyblockcast

allyblockcast Bot commented Aug 3, 2026

Copy link
Copy Markdown

Status check on the latest Ally finding + current CI

Ally's Important Issue from the b4dd2269 review is already fixed at the current head (4767ea9f7eb9db351376d1cc5784b15bbaa7bb03).

retireStaleProductivityReviewReservation (formerly retireTerminalSourceProductivityReviewReservation) now runs the same status/visibility/assignment checks as evaluateSourceReviewability() before finalizing a recovered reservation, and covers every case Ally named:

  • missing_source — a disappeared source no longer leaves the reservation as a permanent null-identifier row; it's retired.
  • terminal_source — unchanged, still retires.
  • unreviewable_source — hidden / in_review / blocked / user-assigned / productivity-review-descendant / opted-out sources are now retired instead of finalized.
  • missing_source_agent / review_owner_changed — reassignment to a different agent (or manager chain) now retires rather than waking the stale owner.

Retirement never allocates an identifier or fires a creation/assignment-wake side effect (verified by 3 new regressions: non-candidate status (in_review), owner reassignment, missing source — all assert created === 0, zero wakeups, zero productivity_review_created/assignment_wake_enqueued activity).

However, CI on this exact head is red, and I don't think it should be re-reviewed or merged until that's resolved:

General tests (server 2/4) — the shard BLO-21003's own verifying signal names — has 5 failing tests, all in productivity-review-service.test.ts (run):

  • does not block monitor claims behind final review issue creation
  • does not let post-reservation monitor claims invalidate the reserved review
  • preserves a Linear-backed reservation when local finalization fails after lookup
  • recovers a stale reserved review only once when reconcilers race (60s timeout)
  • does not treat a fresh assignment wake claim as expired after a long scan

None of these are new — I traced each to the commit that introduced it:

  • does not block monitor claims behind final review issue creationcd58ebe2 ("avoid queue row locks during review create")
  • does not let post-reservation monitor claims invalidate the reserved review6e859e63 ("reserve review before monitor lock release")
  • recovers a stale reserved review only once when reconcilers race00cece6b ("make stale reservation recovery idempotent")
  • preserves a Linear-backed reservation... and ...fresh assignment wake claim...b4dd2269 ("fence wake claim retries")

None of them are from the latest commit (4767ea9f), and none are the 3 new tests it added (those pass). The likely reason this wasn't caught earlier: every one of the ~17 pushes to this branch cancelled the in-flight CI run for the previous head (General tests (server 2/4) shows cancelled on every head before this one), so this is the first time this suite has actually run to completion on this branch — the review-then-push cadence outran CI the whole way through.

Given the PR checklist item "All Paperclip CI gates are green" is unchecked and CI is genuinely red, requesting another Ally pass right now would be premature. Recommend: let CI finish on a head before pushing the next fix (or squash the pending fixes and push once), then re-request review once General tests (server 2/4) and the other currently-failing jobs (verify, e2e, server 3/4, server 4/4) are green.

I'll keep tracking this from BLO-21003.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: fe6e6ba

Prior Findings Dispositioned (1)

  • prior:a37ff01 important 1 — still-present — server/src/services/productivity-review.ts:2698 — Recovery now checks terminal, hidden/non-candidate, reassigned, and missing sources, but those reads are not atomic with finalization. A source can become done, hidden, reassigned, or opted out after validation and before finalizeReservedProductivityReviewIssue(), which does not re-read or lock the source.

Important Issues (3)

  • [prior:a37ff01 important 1] server/src/services/productivity-review.ts:2698 — Stale-reservation recovery has a source-state TOCTOU gap. After all reviewability checks complete, another transaction can make the source unreviewable before line 2699 finalizes the reservation; the finalizer predicates only on the review's null identifier fields, so it can still allocate a Linear issue, record creation, and wake the stale owner. Revalidate the source and finalize or retire the reservation under one coordinating lock/transaction, and make finalization reject a reservation already retired by a concurrent reconciler. Add a regression that transitions the source in this exact pre-finalization window.
  • [code/gstack/native-codex] server/src/services/productivity-review.ts:2676 — Failure to resolve a current review owner authorizes the old owner. If the reserved manager becomes paused, terminated, pending approval, or budget-blocked and no fallback is eligible, resolveReviewOwnerAgentId() returns null; the guarded mismatch condition is skipped and recovery finalizes and wakes review.assigneeAgentId anyway. Treat a null current owner as non-finalizable (retire or defer), matching the normal creation path, and cover an unavailable/budget-blocked owner.
  • [errors/gstack/native-codex] server/src/services/productivity-review.ts:2538 — Retirement can orphan an ambiguously-created Linear side issue. LinearIssueCreateUnconfirmedError deliberately preserves the null-identifier reservation as the stable external idempotency anchor, but if the source becomes unreviewable before lookup confirms the create, this path marks that reservation done without reconciling or deleting the possible Linear issue. Done reservations are excluded from later recovery, so the external issue can remain permanently unlinked. Resolve the stable Linear ID before retirement or retain a durable cleanup state, and add an ambiguous-create-then-source-transition regression.

Strengths

  • New reservations persist the original source-agent identity, and legacy reservations fail closed when source ownership cannot be reconstructed.
  • The added tests cover terminal, non-candidate, owner-change, same-owner reassignment, and missing-source retirement without creation or wake side effects.
  • Linear allocation retains a stable UUID across ambiguous outcomes, and wake replay uses durable idempotency evidence.

Recommended Action

  1. Make source revalidation and reservation finalization/retirement atomic.
  2. Do not finalize when no eligible current review owner exists.
  3. Preserve or clean up the Linear idempotency anchor when retiring an ambiguous reservation.

@allyblockcast allyblockcast left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved with the allyblockcast reviewer seat after green automated review gates.

@allyblockcast
allyblockcast added this pull request to the merge queue Aug 4, 2026
Merged via the queue into master with commit d1cb88d Aug 4, 2026
36 of 37 checks passed
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.

3 participants