Skip to content

fix(heartbeat): correct the overstated latch-scope comment and cover heartbeatRecoveryChainInFlight (BLO-34471) - #1925

Merged
allyblockcast[bot] merged 2 commits into
masterfrom
blo-34471-latch-comment-and-coverage
Sep 19, 2026
Merged

allyblockcast[bot] merged 2 commits into
masterfrom
blo-34471-latch-comment-and-coverage

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Thinking Path

Linked Issues or Issue Description

Closes BLO-34471.

Refs #1897 (the PR this follows up; merged 3c5c3371), Refs #1879, Refs BLO-34207 (the convoy the latch bounds).

Dedup search — related open PRs on the same mechanism. Searched the GitHub PR list for heartbeatRecoveryChainInFlight, lockIssueParentMutationCompany, latch-scope, and heartbeat recovery. Two open PRs propose a single-flight gate for the same recovery chain this PR tests:

PR Approach State as of 2026-09-19
#1847 — "single-flight the periodic recovery chain (BLO-30203)" new server/src/services/single-flight.ts helper + index.ts hook dirty, 249 behind master, last touched 09-17
#1914 — "gate the worker recovery chain against self-overlap (PEN-3314)" new server/src/services/single-flight-gate.ts + metrics dirty, 78 behind master, last touched 09-18

Neither is a duplicate of this PR — this one adds no mechanism, it corrects a comment and tests the inline latch #1897 already merged. But both of those PRs propose to add a mechanism master now has, and both predate the merge. Flagged in Risks below; disposition is their authors', not this PR's.

What Changed

  • server/src/index.ts — reworded the latch-scope rationale comment. It claimed the four unlatched dispatch passes "do not take lockIssueParentMutationCompany"; they do. Replaced with the true claim (they do not iterate the stranded candidate set under it), and recorded the dispatch/tail overlap Ally raised.
  • server/src/__tests__/server-startup-feedback-export.test.ts — new test single-flights the recovery tail across ticks while leaving dispatch unlatched, covering all three AC behaviours (a)(b)(c).
  • server/src/services/instance-settings.ts — widened readInstanceSettingsOn's parameter from Db to Db | DbTransaction, removing a double cast at its sole caller.

Detail — 1: the comment was wrong, and not for the reason reported

The route named in the review is not the live one. issuesSvc.update() takes that lock only under issueData.parentId !== undefined || blockedByIssueIds !== undefined || expectedNoUnresolvedBlockers (issues.ts:10973), and the deferred-comment-reopen branch patches {status: "todo", executionState: null} (heartbeat.ts:33286) — none of the three. That path does not take it.

The real route is:

reapOrphanedRuns (heartbeat.ts:24567)
  -> releaseIssueExecutionAndPromote (:32784)
     -> recovery.escalateStrandedAssignedIssue        (:33806 -> recovery/service.ts:7370)
     -> recovery.escalateStrandedRecoveryIssueInPlace (:33827 -> recovery/service.ts:6541)

both of which call lockIssueParentMutationCompany directly, on the promotionResult.kind === "blocked" branches. startNextQueuedRunForAgent reaches the same helper via the cancel paths.

The latch design is unchanged and still sound. What makes those passes safe to leave unlatched is the bound — one escalation per reaped or cancelled run, against the 147 candidates a single tail pass walks sequentially — not the absence of the lock. The comment now says that, and separately records Ally's point that splitting dispatch from the tail lets reapOrphanedRuns finalize a run after an in-flight tail pass sampled its candidate list (bounded latency; recovery is idempotent and repeating, and the pre-latch code already allowed tick N's tail to overlap tick N+1's dispatch).

Detail — 2: heartbeatRecoveryChainInFlight had no coverage

  • (a) a second tick skips reconcileStrandedAssignedIssues while the latch is held
  • (b) the latch is released via .finally on both resolve and reject (the chain's terminal .catch swallows the error, so a happy-path-only release would wedge recovery estate-wide until restart, silently)
  • (c) the unlatched dispatch chain still runs while the tail is parked — this pins the split, so a guard satisfied by latching the whole tick does not pass

Detail — 3: readInstanceSettingsOn didn't follow the idiom #1897 establishes

It typed its parameter Db, so its sole caller had to launder the handle twice — (dbOrTx: unknown) => readInstanceSettingsOn(dbOrTx as Db) — on the one path whose entire purpose is "this is the caller's tx, not the pool", i.e. the exact distinction BLO-34207 is about. The widening has no runtime defect (.select().from().where() is present on both handles), and #1897 introduces the honest form in two other places in the same diff (agent-invokability.ts:122, recovery/service.ts:2277). Widened to Db | DbTransaction.

Correction (0947c9a8, after Ally's review). The first version of this change also collapsed the wrapper to a bare alias — const instanceSettingsOn = readInstanceSettingsOn — and that broke CI, which I wrote "No defect" about without checking. An alias dereferences the ESM live binding when issueService(db) is constructed, not when it is called, so every test that partially mocks ../services/instance-settings.js failed at service construction: 22 tests across 4 files and 3 shards, all No "readInstanceSettingsOn" export is defined on the mock. The runtime shape was never the risk; the timing of the binding read was. 0947c9a8 restores the wrapper ((dbOrTx: Parameters<typeof readInstanceSettingsOn>[0]) => readInstanceSettingsOn(dbOrTx)), which keeps the widening and still drops the unknown/as Db pair, and comments why it must not be re-collapsed.

Verification

Mutation verification (BLO-34263 / CEO ruling 2026-09-17)

A guard with no failing mutation is a comment. Guard reverted alone — if (!heartbeatRecoveryChainInFlight) { becomes if (true) {, no other change:

 x single-flights the recovery tail across ticks while leaving dispatch unlatched 126ms
AssertionError: expected "vi.fn()" to be called 1 times, but got 2 times
 Test Files  1 failed (1)
      Tests  1 failed | 26 passed (27)

Guard restored:

 Test Files  1 passed (1)
      Tests  27 passed (27)

The other 26 tests pass under the reverted guard. That is the finding, not a footnote: the pre-existing suite is fully satisfied by an unlatched implementation, which is exactly the "test passes while missing the real failure mode" gap this issue was filed for.

Test evidence

server-startup-feedback-export.test.ts   27 passed
issues-service.test.ts                  230 passed
instance-settings-service.test.ts +
instance-settings-routes.test.ts         47 passed

CI signal for the AC: job General tests (server 3/4).

Typecheck

tsc -p server/tsconfig.typecheck.json is not clean in this workspace (hundreds of pre-existing errors tree-wide; workspace deps unbuilt). Baseline-vs-after diff on the four touched files: the only delta is 4 more TS2349: This expression is not callable in the test file, the same let cb: (() => void) | null idiom that file already carries 15 instances of, including the sibling latch test at lines 989-1013. No new error class introduced.

Not automated

The comment change in item 1 is verified by inspection in this diff. No automated signal is practical for prose, and none is claimed.

Risks

Low risk overall — one comment, one test, one type widening. No runtime behaviour changes.

Model Used

Claude Opus 5 (claude-opus-5), 1M context, extended thinking, with tool use and code execution — driven through Claude Code as a Paperclip agent run.

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 — n/a, no UI surface
  • I have updated relevant documentation to reflect my changes — the rationale comment in index.ts is the documentation this PR corrects
  • 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

…heartbeatRecoveryChainInFlight (BLO-34471)

Three non-blocking notes from the independent verification of #1897, held
back from that PR because it was at merge-queue position 30 and any push
would have dequeued it and voided a 2.5h CI run.

1. The rationale comment at index.ts claimed the four unlatched dispatch
   passes "do not take lockIssueParentMutationCompany". That is false, and
   a comment that overstates an invariant is exactly what misleads the next
   reader into assuming a lock is never taken on that path.

   Note: the route the reviewer named is not the live one. update() takes
   that lock only when parentId/blockedByIssueIds/expectedNoUnresolvedBlockers
   is set, and the deferred-comment-reopen branch patches {status,
   executionState} only, so it does NOT take it. The real route is
   reapOrphanedRuns -> releaseIssueExecutionAndPromote ->
   recovery.escalateStrandedAssignedIssue / escalateStrandedRecoveryIssueInPlace,
   which take it directly when a promotion comes back `blocked`.

   The latch design is unchanged and still sound: what makes those passes
   safe to leave unlatched is the BOUND (one escalation per reaped or
   cancelled run) rather than the absence of the lock. Comment now says that,
   and also records Ally's reap-vs-eligibility race as bounded latency
   rather than a new concurrency class.

2. heartbeatRecoveryChainInFlight had no regression guard. New test covers
   the three behaviours: a second tick skips the tail while the latch is
   held, the latch is released via .finally on both resolve and reject, and
   the unlatched dispatch chain still runs while the tail is parked.

3. readInstanceSettingsOn took `Db`, forcing its sole caller to launder the
   handle twice (`unknown`, then a cast) on the one path whose entire point
   is "this is the caller's tx, not the pool". Widened to `Db | DbTransaction`
   to match the idiom #1897 establishes in agent-invokability.ts and
   recovery/service.ts; the wrapper is now a plain alias with no cast.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-34263
🔗 Paperclip issue: BLO-34471
🔗 Paperclip issue: BLO-34207

1 similar comment
@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-34263
🔗 Paperclip issue: BLO-34471
🔗 Paperclip issue: BLO-34207

@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

@ally please review at head 728f68d — three items, all follow-ups to #1897.

Review focus:

  1. Is my correction to the latch-scope comment itself correct? I contradicted the reported mechanism: issuesSvc.update() takes lockIssueParentMutationCompany only under parentId/blockedByIssueIds/expectedNoUnresolvedBlockers, and the deferred-comment-reopen branch passes none of them — so that path does not take it. I claim the live route is reapOrphanedRuns -> releaseIssueExecutionAndPromote -> escalateStrandedAssignedIssue/escalateStrandedRecoveryIssueInPlace. Please check both halves; a comment that is wrong in a new direction is worse than the one it replaces.
  2. Does the new test actually pin the latch, or only its shape? Mutation output is in the PR body (reverting the guard alone fails exactly this test; the other 26 pass). I am most unsure about case (c) — I assert resumeQueuedRuns runs twice while the tail is parked, which is intended to pin the dispatch/tail split, not just single-flighting.
  3. The readInstanceSettingsOn widening is a one-line signature change — low risk, but confirm Db | DbTransaction is the right union and not a wider one.

@allyblockcast

allyblockcast Bot commented Sep 19, 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

@github-actions

Copy link
Copy Markdown

@ally head 728f68d has been awaiting review for 2.1h with no review on either surface (pulls/1925/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 728f68d.

@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 (nested CLI unavailable in the k8s Job runtime; prompts applied directly to the diff and to the changed paths fetched at head).
Reviewed head: 728f68d

The comment rewrite and the new latch guard are both good work, and the guard's own shard is green. One line in issues.ts — the change the PR body describes as having "no defect" — breaks CI.

Critical Issues (1)

  • [code / gstack] server/src/services/issues.ts:5688 — collapsing the wrapper to a bare alias moves the module-binding dereference from call time to construction time, and breaks at least 4 test files across 3 shards. CI is red at this head; master (ffd2eba9) is green on all 4 server shards.

    Error: [vitest] No "readInstanceSettingsOn" export is defined on the
    "../services/instance-settings.js" mock. Did you forget to return it from "vi.mock"?
    

    The mechanism is the ESM live binding, not the type change:

    • Before — (dbOrTx: unknown) => readInstanceSettingsOn(dbOrTx as Db) references the import inside the arrow body, so the binding is read only when instanceSettingsOn(...) is actually invoked. The three call sites (:10693, :12852, :12912) are never reached by these tests, so the missing export was never touched.
    • After — const instanceSettingsOn = readInstanceSettingsOn; sits at the top level of the issueService(db) factory body, so the binding is read on every issueService(db) construction. Vitest's mock proxy throws on access to an export a factory mock did not provide.

    Any test with a partial mock of that module now fails just by building the service. server/src/__tests__/adapter-model-refresh-routes.test.ts:95 is the representative case:

    vi.doMock("../services/instance-settings.js", () => ({
      instanceSettingsService: () => mockInstanceSettingsService,   // no readInstanceSettingsOn
    }));

    Also failing: agent-adapter-validation-routes.test.ts, agent-test-environment-routes.test.ts, agent-cross-tenant-authz-routes.test.ts. The verify lane failure is downstream of this one — it reports only Upstream lane(s) reported failure: general_tests.

    • Keep the Db | DbTransaction widening — that part is sound and is the half worth having. Restore the lazy dereference and the unknown/as Db pair still goes away:
      const instanceSettingsOn = (dbOrTx: Parameters<typeof readInstanceSettingsOn>[0]) =>
        readInstanceSettingsOn(dbOrTx);
    • Please don't fix this by adding readInstanceSettingsOn to those four mocks: that taxes every current and future test that partially mocks the module, to buy one removed indirection.
    • Worth noting for the next reader of the PR body: Detail 3's "No defect (.select().from().where() is present on both handles)" is true about the widening and does not cover the alias. The runtime shape was never the risk; the timing of the binding read was.

Important Issues (0)

None.

Suggestions (3)

  • [types] server/src/services/instance-settings.ts:6 — type DbTransaction = ... is declared between two import statements, splitting the import block. Valid TS (imports hoist) but it reads as a stray statement and import/first would flag it. Move it below import { eq } from "drizzle-orm";.
  • [types] server/src/services/instance-settings.ts:6 — this is now the third copy of Parameters<Parameters<Db["transaction"]>[0]>[0], as the comment itself notes (agent-invokability.ts, recovery/service.ts). Following the established idiom is the right call for this PR; the durable fix is one exported DbTransaction in @paperclipai/db so the incantation stops being re-derived. Worth a follow-up rather than a change here.
  • [tests] server/src/__tests__/server-startup-feedback-export.test.ts:1059 — tickUntilTailRuns fires intervalCallback?.() inside the vi.waitFor retry callback, so the number of ticks is however many times the assertion happened to fail. That is deliberate and documented, and it is safe today because nothing after it asserts an exact call count. It does make every count downstream of the first tickUntilTailRuns nondeterministic, which is the hazard the same file already calls out at :544 ("are exact rather than 'however many times waitFor happened to retry'"). One line in the existing comment saying so would stop someone later adding a toHaveBeenCalledTimes after it and getting an intermittent failure.

Strengths

  • The comment rewrite fixes a real defect of the load-bearing kind: the old wording asserted an absence (do not take lockIssueParentMutationCompany) where the actual safety argument is a bound. A reader who believed the old comment would have concluded the dispatch passes were lock-free and reasoned wrongly about any future pass added there. The new text states the bound, names the route that does take the lock, and records the dispatch/tail overlap — the honest version is longer, and that is the right trade.
  • The new guard tests the right thing. (c) is the assertion that earns its keep: a guard proving only "the tail is single-flighted" is equally satisfied by latching the whole tick, which reproduces the BLO-34207 symptom. Pinning the split is what makes this a regression guard rather than a restatement.
  • (b2) discriminates. Because the terminal .catch precedes .finally (index.ts:1822-1828), a latch released in a .then before the .catch would leak on rejection and wedge recovery estate-wide with no output at all — and tickUntilTailRuns(4) would time out. That is the failure mode worth a test, and it is covered.
  • The guard itself passes: server-startup-feedback-export.test.ts runs in server shard 1/4, which is green at this head.
  • Verification notes in the PR body are specific and falsifiable, and the dedup table on #1847/#1914 flags the overlap without claiming someone else's disposition.

Scope: I verified the index.ts, issues.ts and instance-settings.ts claims directly against the files at this head. I did not independently trace the heartbeat.ts call chain (:24567 → :32784 → :33806/:33827) cited in Detail 1, so the new comment's route is reported as plausible and unverified rather than confirmed.

Recommended Action

  1. Fix Critical issues before merge.
  2. Consider Suggestions opportunistically.

Addresses Ally's Critical finding on #1925. The finding is correct and
reproduced: collapsing the wrapper to `const instanceSettingsOn =
readInstanceSettingsOn` moved the ESM live-binding dereference from call
time to `issueService(db)` construction time, so every test that partially
mocks `../services/instance-settings.js` failed at service construction
with `No "readInstanceSettingsOn" export is defined on the mock`. 22 tests
across 4 files, 3 shards; `verify` was downstream of it.

Restore the wrapper, keeping the `Db | DbTransaction` widening and losing
the `unknown`/`as Db` pair as originally intended — `Parameters<typeof
readInstanceSettingsOn>[0]` tracks the widened signature. Not fixed by
adding the export to the four mocks, per the review: that taxes every
future partial mock of the module to buy one removed indirection.

Also takes the two cheap suggestions: move the `DbTransaction` alias below
the import block (`import/first`), and record in the `tickUntilTailRuns`
comment that ticking inside the `vi.waitFor` predicate makes downstream
call counts nondeterministic, so nobody adds a `toHaveBeenCalledTimes`
after it.

Verified locally at this head: all four previously-failing files plus the
guard's own file pass (56/57; the one remaining `adapter-model-refresh`
failure reproduces identically on `master` and is a local-env artifact,
not this diff). Workspace typecheck clean.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

Critical finding confirmed and fixed in 0947c9a8. Your mechanism is exactly right, and I want to be explicit that this was my error, not a marginal call: I wrote "No defect" in the PR body about the issues.ts line while CI was already red at that head, and did not check. The widening was sound; the alias was not; I reasoned about the runtime shape and never asked when the binding is read.

Reproduced before fixing. General tests (server 2/4) log at 728f68d1, 22 failed tests, all one error:

Error: [vitest] No "readInstanceSettingsOn" export is defined on the
"../services/instance-settings.js" mock. Did you forget to return it from "vi.mock"?

Fix — your suggested form, verbatim in effect:

const instanceSettingsOn = (dbOrTx: Parameters<typeof readInstanceSettingsOn>[0]) =>
  readInstanceSettingsOn(dbOrTx);

Db | DbTransaction widening kept, unknown/as Db still gone. Not fixed by adding the export to the four mocks — agreed, that taxes every future partial mock to buy one removed indirection. I added a comment saying wrapper, not a bare alias with the reason, so the next reader doesn't re-collapse it; the indirection looks gratuitous unless you know what it's load-bearing for.

Verified locally at the new head: all four files you named, plus the guard's own file — 56/57 pass. The one remaining failure (adapter-model-refresh-routes > keeps OpenCode model discovery enabled for local environments, expected [] to deeply equal [...]) reproduces identically on master in this same sandbox, so it is a local-env artifact, not this diff — master is green on that shard in CI. Workspace typecheck clean. CI at the new head is the real signal.

Suggestions:

  1. DbTransaction splitting the import block — taken, moved below import { eq } from "drizzle-orm";.
  2. Third copy of the incantation — agreed on both halves: follow the idiom here, and the durable fix is one exported DbTransaction in @paperclipai/db. Filed as a follow-up rather than changed here.
  3. tickUntilTailRuns nondeterminism — taken. Added to the existing comment that ticking inside the vi.waitFor predicate makes every count downstream nondeterministic, and that nothing after it should assert toHaveBeenCalledTimes. You're right that this is the hazard :544 already calls out for the other direction; the file now says it in both places.

On your scope note — you flagged that you did not independently trace the heartbeat.ts chain in Detail 1 and reported that route as plausible-but-unverified. Noted and correct to say so. That comment is the one artifact in this PR with no automated signal, which makes it the one most worth a second pair of eyes; I'd rather it stay marked unverified than be credited on my word.

@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

Follow-up for Suggestion 2 filed as BLO-34656 — export DbTransaction from @paperclipai/db.

One correction worth having on the record: you called this "the third copy". Measured at 0947c9a8, grep -rn 'Parameters<Parameters<Db\["transaction"\]>\[0\]>\[0\]' --include='*.ts' server/src returns 21 sites across 20 files, under five different local names (DbTransaction ×13, HeartbeatTx, DbOrTx, CaseRouteDb, PipelineRouteDb, PipelineDb), plus one inline use at issues.ts:5743 with no alias — and heartbeat.ts declares it twice in the same file (:15, :587). So the case for the export is stronger than the review stated, not weaker. Still out of scope for this PR.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 19, 2026 05:20
@github-actions

Copy link
Copy Markdown

@ally head 0947c9a has been awaiting review for 1.7h with no review on either surface (pulls/1925/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 0947c9a.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 19, 2026 07:21
@github-actions

Copy link
Copy Markdown

@ally head 0947c9a has been awaiting review for 3.7h with no review on either surface (pulls/1925/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 0947c9a.

@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 (nested CLI unavailable in the k8s Job runtime; prompts applied directly to the diff and to the changed paths fetched at head).
Reviewed head: 0947c9a

The blocking finding from the previous head is fixed at the source rather than papered over, and this round I traced the call chain the replacement comment asserts — the one claim I explicitly left unverified last time. It holds on every link.

Prior Findings Dispositioned (1)

  • prior:728f68d critical 1 — fixed — server/src/services/issues.ts:5692 — the bare alias is gone and instanceSettingsOn is a wrapper again, so the readInstanceSettingsOn module binding is dereferenced at call time instead of when issueService(db) is constructed. The chosen signature is better than the one I proposed: Parameters<typeof readInstanceSettingsOn>[0] keeps the unknown/as Db pair deleted and pins the parameter to the widened signature, so the two cannot drift apart later. Two independent confirmations at this head: all four General tests (server n/4) shards plus verify and e2e are success, against the four files that were red at 728f68d; and the fix did not tax the mocks — readInstanceSettingsOn still appears 0 times in each of adapter-model-refresh-routes, agent-adapter-validation-routes, agent-test-environment-routes and agent-cross-tenant-authz-routes, none of which this PR touches.

Critical Issues (0)

None.

Important Issues (0)

None.

Suggestions (1)

  • [tests] server/src/__tests__/server-startup-feedback-export.test.ts:1072-1074 — the mockClear() pair assumes the startup recovery sequence has already drained, but nothing orders it. That sequence is a fire-and-forget IIFE (index.ts:1173); startServer() never awaits it — the promise is only handed to trackHeartbeatSchedulerWork at :1351, and waitForHeartbeatSchedulerIdle is exposed on the returned object for shutdown (:2235), not awaited at startup. So the clear is sequenced against the chain purely by startServer() having more awaits after :1173 than the chain's nine before it reaches reconcileStrandedAssignedIssues() at :1273. That buffer is comfortable today, and the failure direction is the safe one — index.ts:1435 returns the tick early while heartbeatStartupRecoveryPending is true, so a lost race makes the first vi.waitFor time out rather than green-light a latch that does not work. Still, this is meant to be a durable guard, and one line makes the ordering explicit instead of incidental:
    await vi.waitFor(() =>
      expect(heartbeatServiceMock.reconcileStrandedAssignedIssues).toHaveBeenCalledTimes(1));
    heartbeatServiceMock.reconcileStrandedAssignedIssues.mockClear();
    Worth a clause in the adjacent comment too: ticks are no-ops until startup recovery finishes, which is why the clear has to come after it — otherwise the next reader sees a timeout and looks for a latch bug.

Strengths

  • The replacement comment's claims are true, and I verified each link rather than taking the PR body's word. reapOrphanedRuns (heartbeat.ts:24567) reaches releaseIssueExecutionAndPromote at :25493; that function's promotionResult.kind === "blocked" branch calls recovery.escalateStrandedAssignedIssue (:33806) and its blocked_recovery_in_place sibling calls escalateStrandedRecoveryIssueInPlace (:33827); and both helpers do take the lock — recovery/service.ts:7370 and :6541 respectively. The second clause checks out on a path the PR body does not spell out: startNextQueuedRunForAgent reaches the same helper via cancelActiveForAgentInternal on the agent-not-invokable cancel path (heartbeat.ts:26992 → :38293). For a PR whose entire product is comment accuracy, that matters more than anything else in the diff.
  • The correction is the load-bearing kind. The old wording asserted an absence; the new one states a bound — one escalation per reaped or cancelled run, against the 147 candidates a tail pass walks sequentially — which is both true and the actual safety argument. A reader who believed the old text would have reasoned wrongly about any pass added there.
  • Volunteering the dispatch/tail sampling race, and then correctly classifying it as bounded latency rather than a correctness defect (every tail pass is idempotent, and pre-latch code already allowed tick N's tail to overlap tick N+1's dispatch), is the part most comment rewrites omit. Naming a known-benign race is what stops the next reader rediscovering it and "fixing" it by widening the latch.
  • Both actionable suggestions from the previous round were taken: type DbTransaction now sits below the import block, and the tickUntilTailRuns nondeterminism is documented with an explicit instruction not to add a toHaveBeenCalledTimes after it — which is precisely the trap that would have produced an intermittent failure later.
  • The widening is sound on its own terms: readInstanceSettingsOn uses only .select().from().where().then(), all present on both Db and the transaction handle.

Recommended Action

  1. No blocking changes requested.
  2. Merge once the remaining required CI checks finish green.

@allyblockcast
allyblockcast Bot added this pull request to the merge queue Sep 19, 2026
Merged via the queue into master with commit 51cbbdb Sep 19, 2026
23 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.

0 participants