Skip to content

fix(agent): re-register the push service when a heartbeat gets HTTP 404 - #4332

Merged
kwakayama merged 6 commits into
mainfrom
issue/873-heartbeat-persistent-failure
Aug 31, 2026
Merged

fix(agent): re-register the push service when a heartbeat gets HTTP 404#4332
kwakayama merged 6 commits into
mainfrom
issue/873-heartbeat-persistent-failure

Conversation

@kwakayama

@kwakayama kwakayama commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

When the control plane answers a heartbeat with HTTP 404 for a previously registered service id (registry row evicted, reset, or redeployed), the registration lifecycle had no recovery path: registerAgentPushRuntimeService was called exactly once at lifecycle creation, so every subsequent tick failed and the "Agent service heartbeat failing persistently" escalation fired forever. This change detects a heartbeat failure classified as NETWORK_ERROR with upstream HTTP status 404 and re-runs registration inside the heartbeat catch (the service_key upsert makes it idempotent), adopts the returned service row for subsequent heartbeats, and resets the consecutive-failure counter on a recovered tick so the persistent-failure escalation never fires when re-registration works. Re-registration failures still count toward the escalation so a genuinely dead control plane surfaces, the teardown AbortController signal is threaded through the re-registration request, and stop() semantics, per-tick 4xx non-retry behavior, and the transient 5xx/transport retry schedule are unchanged.

Fixes veryfront/veryfront-issue-inbox#873

Red

deno task test:file src/agent/service/registration.test.ts
agent/agent-service-registration heartbeat recovery ...
  re-registers a service the control plane no longer knows instead of failing persistently ... FAILED (133ms)

error: AssertionError: Values are not equal: a lost registration must trigger re-registration, not the persistent-failure escalation
    [Diff] Actual / Expected
-   [
-     "Agent service heartbeat failing persistently",
-   ]
+   []
    at src/agent/service/registration.test.ts:1094:5

FAILED | 2 passed (21 steps) | 1 failed (1 step) (9s)

Green

deno task test:file src/agent/service/registration.test.ts

agent/agent-service-registration heartbeat recovery ...
  re-registers a service the control plane no longer knows instead of failing persistently ... ok (87ms)
ok | 3 passed (22 steps) | 0 failed (9s)

Full directory suite and static checks:

deno task test:file src/agent/service/   ->  ok | 58 passed (145 steps) | 0 failed (14s)
deno lint src/agent/service/registration.ts   ->  Checked 1 file
deno check src/agent/service/registration.ts src/agent/service/registration.test.ts   ->  clean

Revert check

With the fix commit reverted (git revert --no-commit 6dc734cd3, only src/agent/service/registration.ts modified, test commit untouched), the new test fails for the issue's reason: FAILED | 2 passed (21 steps) | 1 failed (1 step) (9s) — the persistent-failure error is logged and no second registration occurs. Restored to HEAD (git reset --hard 6dc734cd3), the same command passes: ok | 3 passed (22 steps) | 0 failed (9s).

Acceptance criteria

The inbox issue is labeled repo:veryfront-agent, but the erroring code lives in this repo's framework at src/agent/service/registration.ts (veryfront-agent only consumes the lifecycle via src/agent/hosted/cloud-agent-chat-execution.ts), so the branch was cut here.

Deployment gates

  • Merge only after the exact-head CI matrix is complete and green, all review threads are resolved, and both independent review gates meet the required score.
  • Release through the normal veryfront-code package pipeline; do not apply production-data changes or suppress Sentry. The veryfront-agent release owner must verify the deployed agent image resolves to a framework version containing commit d5adcaa before calling the fix deployed.
  • If the release cannot be verified or the heartbeat regression recurs, roll the agent deployment back to the previous known-good framework version and reopen this PR/issue follow-up rather than weakening the alert.

Post-deploy Sentry verification

  • Owner: the veryfront-agent release/on-call owner for the deployment.
  • After production rollout, monitor Sentry VERYFRONT-AGENT-E for at least 24 hours and confirm there are no new events with the signature "Agent service heartbeat failing persistently" attributable to a heartbeat 404 followed by successful re-registration.
  • Correlate any recovery event with the "Agent service re-registered after the control plane lost its registration" log and verify subsequent heartbeats target the adopted service id without a persistent-failure escalation.
  • Keep issue veryfront/veryfront-issue-inbox#873 open until the production version is recorded and the 24-hour quiet period completes; close it only when the group is quiet or explicitly classified with follow-up ownership.

@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 4 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f6c977d2-6506-4fe0-a3ac-2f4b9e8e0e49

📥 Commits

Reviewing files that changed from the base of the PR and between b4fc9ed and d5adcaa.

📒 Files selected for processing (2)
  • src/agent/service/registration.test.ts
  • src/agent/service/registration.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

📦 Client bundle boundary

Entrypoint Modules Source size Server leaks
src/index.client.ts 288 2215 KiB ✅ 0

A server module in a client graph aborts hydration in the browser. New leaks fail CI; known leaks are tracked in scripts/lint/client-bundle-baseline.json to burn down.

Comment thread src/agent/service/registration.ts
Comment thread src/agent/service/registration.ts
@codecov

codecov Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.32258% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/agent/service/registration.ts 90.32% 4 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@kwakayama
kwakayama marked this pull request as ready for review August 31, 2026 06:00

@greptile-apps greptile-apps 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.

kwakayama has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-08-31T06:49:48.382852Z d5adcaa New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown

@codex review

@gitar-bot

gitar-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

Note

Automatic reviews are paused because your trial's included automatic processing has been used for this period. Upgrade now, or comment "Gitar review" to run a review anytime.
Learn more

Code Review ✅ Approved 2 resolved / 2 findings

Detects HTTP 404 on heartbeat and re-registers the push service instead of escalating to persistent failure. After re-registration succeeds, heartbeats resume and the consecutive-failure counter resets. Re-registration failures still count toward escalation, and existing 4xx non-retry, 5xx retry, and teardown semantics are unchanged. All tests pass, lint and typecheck are clean.

✅ 2 resolved
Edge Case: Endless re-register loop never escalates if 404s persist

📄 src/agent/service/registration.ts:571-585 📄 src/agent/service/registration.ts:623-637
If the control plane accepts each re-registration POST but keeps answering every subsequent heartbeat with 404 (registry that immediately evicts the row, a broken/dead control plane that still serves the registration endpoint), each tick classifies the 404 as a lost registration, re-registers successfully, sets reRegisteredAfterLostRegistration=true, and the .catch at line 629 resets consecutiveHeartbeatFailures to 0. The 'Agent service heartbeat failing persistently' escalation can therefore never fire, and the lifecycle silently issues one extra registration POST per interval forever — reintroducing the very unbounded-failure behavior this PR set out to fix, just relocated. Consider bounding recovery: track consecutive re-registrations (or ticks where no heartbeat actually succeeded) and let that count toward the escalation once it exceeds a threshold, so a control plane that keeps forgetting the service still surfaces.

Quality: Re-registration failure branch has no test coverage

📄 src/agent/service/registration.ts:583-593 📄 src/agent/service/registration.test.ts:1058-1072
The new catch branch (lines 583-593) that logs 'Agent service re-registration failed' and lets the failure keep counting toward the escalation is untested; the only added test covers the successful-recovery path. Add a colocated test in registration.test.ts where the heartbeat 404s and the re-registration POST also fails, asserting the warn is logged and the persistent-failure escalation still fires after three ticks, so the 'genuinely dead control plane still surfaces' guarantee is regression-protected.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a986035b0e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agent/service/registration.ts Outdated
Comment thread src/agent/service/registration.ts Outdated
Comment thread src/agent/service/registration.ts Outdated

@greptile-apps greptile-apps 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.

kwakayama has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@github-actions

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c09631474

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agent/service/registration.ts Outdated
Comment thread src/agent/service/registration.ts Outdated
Comment thread src/agent/service/registration.ts Outdated

@greptile-apps greptile-apps 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.

kwakayama has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@github-actions

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 34713d49f5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@greptile-apps greptile-apps 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.

kwakayama has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@github-actions

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: eb2a493483

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@kwakayama

kwakayama commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Codex independent review for the exact head eb2a493483f10ec43c9b4f5aaa393e8195d5b38e.

Finding, P2 medium:

  • The PR body Acceptance criteria section does not document the deployment gates or post-deploy Sentry verification required by linked issue fix(runtime): persist provider-executed streamed tool results #873. The body records the root cause, scope, red -> green evidence, and local checks, but gives no rollout gate, deployment condition or owner, Sentry verification query or time window, or closure signal. Add those operational details before merge so the high-priority Sentry regression has a verifiable rollout and post-deploy check. Evidence: linked issue fix(runtime): persist provider-executed streamed tool results #873, Acceptance criteria bullet requiring deployment gates and post-deploy Sentry verification; PR body, Acceptance criteria section. This is the only actionable finding.

Assessment:

  • Correctness and completeness: the 404 recovery, adopted service state, bounded recovery request, repeated-loss escalation, teardown checks, and promise-scoped recovery state are covered by the implementation. No additional code correctness finding.
  • Regression tests and verification: pinned local Deno checks pass: focused registration suite, 27 steps; service-directory suite, 150 steps; lint, typecheck, format check, and diff check all pass.
  • Reliability and security: no additional finding.
  • Repository standards and maintainability: changed files are scoped to the registration implementation and colocated tests.
  • Existing review state: all 8 existing review threads are resolved. At the metadata snapshot, 23 checks were completed successfully, 12 were in progress, 9 were skipped, and no check reported failure. Pending GitHub checks were not awaited.

Verdict: REQUEST_CHANGES until the rollout and post-deploy Sentry verification contract is added to the PR body or linked operational documentation.

Score: 84/100

Review-Gate:
Reviewer: Codex
Reviewed-SHA: eb2a493
Score: 84/100
Actionable-Findings: 1
Verdict: REQUEST_CHANGES

@kwakayama

Copy link
Copy Markdown
Contributor Author

Codex independent review for the exact head eb2a493483f10ec43c9b4f5aaa393e8195d5b38e.

Findings:

  • None. The 404 classification is restricted to the existing NETWORK_ERROR status context, recovery re-registers with the existing service key, adopts the returned service row before the next beat, and preserves the existing failure escalation for failed or repeated recovery. The recovery result is scoped to the exact scheduled heartbeat promise, and teardown is checked before state publication (src/agent/service/registration.ts:496-619).
  • The linked issue acceptance criteria are covered by colocated tests for successful recovery, repeated lost registrations, failed and hung re-registration, direct-caller concurrency, and stop teardown (src/agent/service/registration.test.ts:1086-1334).
  • The current PR body now links issue fix(runtime): persist provider-executed streamed tool results #873 and documents deployment gates plus post-deploy Sentry verification.

Verification:

  • deno task test:file src/agent/service/registration.test.ts: 3 passed, 27 steps, 0 failed.
  • deno task test:file src/agent/service/: 58 passed, 150 steps, 0 failed.
  • deno lint src/agent/service/registration.ts src/agent/service/registration.test.ts: passed.
  • deno check src/agent/service/registration.ts src/agent/service/registration.test.ts: passed.
  • deno fmt --check src/agent/service/registration.ts src/agent/service/registration.test.ts: passed.
  • git diff --check for the exact base-to-head comparison: passed.
  • Review-thread pagination is complete: 8 threads, all resolved. The head remained stable during the gate snapshot.
  • GitHub checks currently show 40 passed, 0 failed, 8 skipped, and 1 pending Sonar check. The pending check was not awaited, per the review request, and remains a merge-gate verification gap.

Verdict: APPROVE for code review. No actionable findings remain; merge still requires the pending check and repository review-gate conditions to complete.

Score: 95/100

Review-Gate:
Reviewer: Codex
Reviewed-SHA: eb2a493
Score: 95/100
Actionable-Findings: 0
Verdict: APPROVE

@kwakayama

Copy link
Copy Markdown
Contributor Author

Claude Code Review — eb2a4934

Scope verification

The PR branch forks from 32dac4af8 (merge-base), which is 2 commits behind the declared base 0e2d942 (main tip). Diffing against the merge-base confirms the PR touches exactly 2 files:

File +/-
src/agent/service/registration.ts +90
src/agent/service/registration.test.ts +299

The server handler and durable-run-event-sink diffs visible in the base..head comparison are merge-base artifacts from PRs #4325 and #4326 landing on main after this branch was cut. They disappear with a rebase and are not reviewed here.


Correctness and completeness (38/40)

Recovery logic is sound. Traced every control-flow path at registration.ts:572–619:

  1. isLostRegistrationHeartbeatFailure (registration.ts:496–498) correctly gates on NETWORK_ERROR.slug + HTTP 404 — no false positives from other 4xx codes.
  2. Re-registration uses retryWithBackoff with maxAttempts: 1 and timeoutMs: Math.max(heartbeatIntervalMs, HEARTBEAT_MIN_ATTEMPT_TIMEOUT_MS) (registration.ts:578–587), bounding the recovery attempt to a single try with a deadline matching the existing heartbeat timeout floor. Verified HEARTBEAT_MIN_ATTEMPT_TIMEOUT_MS is 5000ms.
  3. Bounded exemption: awaitingHeartbeatAfterReregistration starts false. First recovery sets it true and adds the current promise to recoveredHeartbeats. A second 404 before any heartbeat succeeds finds the flag already true, skips the WeakSet add, and the interval handler increments consecutiveHeartbeatFailures normally → escalation at 3 ticks. A successful heartbeat resets the flag (registration.ts:565), granting one fresh recovery.
  4. Promise-scoped state: recoveredHeartbeats (WeakSet<Promise<void>>) at registration.ts:544 keys recovery status to the exact shared promise captured at registration.ts:644 (const scheduledHeartbeat = heartbeat()). This eliminates the race where a direct caller's rejection handler starts a new heartbeat that clears mutable state before the interval's .catch reads it. Verified the WeakSet lookup at registration.ts:648 matches the interval-captured reference.
  5. Stop guards: Four if (stopped) return checks at lines 569, 589, 604, 615 cover every await boundary (heartbeat, re-registration success, re-registration failure, post-recovery). Late-arriving re-registrations are not adopted.
  6. Failed recovery rethrows registrationError (not the original 404), so the third-tick escalation reports the actual recovery failure (e.g. HTTP 500), not the stale 404. Confirmed by test assertion at registration.test.ts:1262.
  7. lifecycle.serviceId and lifecycle.service are updated at registration.ts:597–598 on successful recovery. The AgentServiceRegistrationLifecycle type (registration.ts:196–201) declares both as plain mutable fields. The lifecycle object is a const binding to a plain object (registration.ts:670), so property assignment works correctly under strict mode.

Minor note (non-blocking): After successful re-registration the heartbeat promise still rejects with the original 404 via throw error at line 619. Direct callers of lifecycle.heartbeat() see a rejection even though recovery worked; the interval handler treats it as recovered via the WeakSet check. The PR body documents this as intentional ("a direct caller sees the failed beat"). The contract is correct but subtle — worth a code comment if callers multiply.

Tests (20/20)

Six new test cases in a dedicated "heartbeat recovery" describe block, all verified locally (27 steps, 10s):

Test What it proves
re-registers on 404 Happy-path: second POST, adopted ID exposed on lifecycle.serviceId and lifecycle.service, recovered heartbeats succeed, no error-level log
repeated loss escalates Bounded exemption: 4+ registration attempts, escalation at consecutiveFailures: 3
failed re-registration escalates Dead control plane: 3 warn logs + escalation at 3, error metadata reports HTTP 500
hung re-registration times out FakeTime: 3 abort events, escalation at 3 — proves the retryWithBackoff timeout works
recovery scoped to scheduled promise Race condition: direct caller starts next heartbeat during recovery, no false escalation
post-stop non-adoption Deferred fetch: stopped lifecycle retains original ID, no recovery log

scriptedHeartbeatFetch helper cleanly extended with heartbeatResponse and registrationResponse callbacks and registrationAttempts() counter. recordingLogger extended to capture info entries.

Codecov: 90.32% patch coverage. The 4 missing + 2 partial lines are defensive stopped return guards and optional logger calls — low-risk branches that are structurally protected by the surrounding test scenarios.

Reliability and security (14/15)

  • Teardown AbortSignal threaded through recovery registration (registration.ts:582)
  • WeakSet for recovery state prevents memory leaks from long-running lifecycles
  • No new external inputs, no new network-facing surface, no credential handling changes
  • Minor: SonarQube quality gate shows 71.4% coverage < 80% required, but this is against the 0e2d942 base which includes merge-base artifact files. After rebase, the gate should reflect the actual ~90% patch coverage. The sonar check was still IN_PROGRESS at review time.

Maintainability (14/15)

  • isLostRegistrationHeartbeatFailure is a focused, well-documented predicate mirroring the existing isRetryableHeartbeatFailure pattern
  • Recovery logic is contained within the existing heartbeat catch block; no new public API surface
  • The lifecycle variable hoisting (const lifecycle before return at line 670) is a clean structural change enabling recovery to synchronize fields
  • Minor: Two recovery-tracking mechanisms (awaitingHeartbeatAfterReregistration flag for bounding exemptions, recoveredHeartbeats WeakSet for promise-scoped state) serve complementary purposes but increase cognitive load for future maintainers. Both are necessary — the flag tracks "has a heartbeat succeeded since last recovery" while the WeakSet tracks "which promise instance was recovered" — but a brief comment explaining their interaction would help.

Scope, docs, and rollout (9/10)

  • PR body is thorough: root cause, red→green evidence, revert check, acceptance criteria, deployment gates, post-deploy Sentry verification with 24-hour quiet-period contract
  • All 8 prior review threads are resolved with follow-up commits (a986035 through eb2a493)
  • The Codex review finding about missing deployment documentation has been addressed — the PR body now includes comprehensive deployment gates and Sentry verification sections
  • Rebase needed before merge: the branch is 2 commits behind main (#4325 fail-closed shared-runtime gates, #4326 durable-run terminal handling). No conflict is expected since the PR only touches registration.ts and registration.test.ts.

Verification

Check Result
deno task test:file src/agent/service/registration.test.ts ok, 3 passed (27 steps), 0 failed (10s)
deno check registration.ts registration.test.ts clean
deno lint registration.ts Checked 1 file
CI matrix (at review time) All functional checks pass; sonar pending
Merge-base diff (32dac4af8..eb2a4934) 2 files only, confirms clean scope

Gaps

  • SonarQube gate resolution depends on rebase (informational, not a code defect)
  • No integration-level test against a real control-plane stub returning 404 (unit coverage is sufficient for this fix)

Verdict

The implementation is correct, well-bounded, and thoroughly tested. Every edge case identified during prior reviews (unbounded recovery loop, hung registration, promise-scoped state race, post-stop adoption, writable lifecycle fields) has been addressed with a corresponding regression test. The only pre-merge requirement is a rebase onto current main.

Score: 95/100

Review-Gate:
Reviewer: Claude
Reviewed-SHA: eb2a493
Score: 95/100
Actionable-Findings: 0
Verdict: APPROVE

@kwakayama
kwakayama added this pull request to the merge queue Aug 31, 2026
Kentaro Wakayama and others added 5 commits August 31, 2026 08:45
A heartbeat answered with 404 means the control plane no longer knows the
service id (registry row evicted, environment reset, redeploy). The 404 is
correctly non-retryable within a tick, but nothing ever registered the
service again, so every subsequent tick failed and the lifecycle logged
"Agent service heartbeat failing persistently" forever (Sentry
VERYFRONT-AGENT-E, issue-inbox#873).

On a lost-registration failure the lifecycle now registers again (the
service_key upsert makes it idempotent), adopts the returned id for later
heartbeats, and keeps the recovered tick out of the consecutive-failure
counter. A failed re-registration still counts toward the escalation so a
genuinely dead control plane surfaces persistently, and the teardown abort
signal is threaded through the re-registration request.
@kwakayama
kwakayama removed this pull request from the merge queue due to a manual request Aug 31, 2026
@kwakayama
kwakayama force-pushed the issue/873-heartbeat-persistent-failure branch from eb2a493 to d5adcaa Compare August 31, 2026 06:49

@greptile-apps greptile-apps 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.

kwakayama has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@github-actions

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: d5adcaafc4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@kwakayama

Copy link
Copy Markdown
Contributor Author

Claude Code Review — d5adcaafc

Scope & root cause

Sentry VERYFRONT-AGENT-E (issue-inbox#873): when the control plane evicts/resets a registered service row, every heartbeat for the stale ID returns HTTP 404. Since 404 is non-retryable, three ticks trigger the "Agent service heartbeat failing persistently" escalation — and nothing ever re-registers, so it repeats forever.

Diff summary (three-dot b4fc9edc...d5adcaafc)

File +/- Purpose
src/agent/service/registration.ts +74 Detect heartbeat 404, re-register, adopt new service row, isolate failure counter for recovered heartbeats
src/agent/service/registration.test.ts +257 6 new recovery tests + extended test infrastructure

6 commits showing clean TDD discipline (red first, then green, then hardening: timeout bounding, exposure of adopted registration, recovery state isolation).

Correctness (38/40)

DetectionisLostRegistrationHeartbeatFailure (:496-498) correctly narrows to NETWORK_ERROR + httpStatus === 404, matching the error shape produced by readAgentPushRuntimeServiceResponse (:368-372). No risk of misfiring on other error types or slugs.

Recovery path (:572-618) — On 404, calls retryWithBackoff with maxAttempts: 1 (single attempt + timeout, no actual retry). Timeout is Math.max(heartbeatIntervalMs, HEARTBEAT_MIN_ATTEMPT_TIMEOUT_MS), matching the heartbeat timeout logic. teardown.signal threaded through so stop() aborts in-flight re-registration.

State machine — Two flags control failure-counter exemption:

  • awaitingHeartbeatAfterReregistration (:543): only the first recovery is exempted; a second 404 before any heartbeat succeeds means the control plane is repeatedly losing registrations → counts toward escalation.
  • recoveredHeartbeats WeakSet (:544): tags the in-flight promise so the interval handler resets the counter instead of incrementing it.

Traced the state transitions for all scenarios:

  1. Happy path: 404 → re-register → recovered promise tagged → counter reset → next heartbeat uses new ID → succeeds → awaitingHeartbeatAfterReregistration cleared.
  2. Repeated loss: first recovery exempted, subsequent 404s increment counter → escalation on 3rd.
  3. Failed re-registration: throw registrationError exits the function; original throw error unreachable; interval handler increments counter; escalation reports the registration error (more actionable than the 404).

Post-recovery throw: after successful re-registration, the heartbeat still throws the original 404 (throw error at :619). This is intentional — direct callers see the failed beat, but the interval handler recognizes the recovered promise and resets the counter. Subtle but correct.

Lifecycle mutation (:596-598): service (closure let), lifecycle.serviceId, and lifecycle.service are all updated, so subsequent heartbeats use the new service ID and external callers see the adopted row.

stop() guards: three if (stopped) return; checks after each async boundary (:589, :603-606, :614-616) plus the teardown signal abort. Verified by the "does not adopt or publish a re-registration that finishes after stop" test.

Minor deduction: the two-phase recovery state (awaitingHeartbeatAfterReregistration + recoveredHeartbeats) is correct but its invariants are distributed across ~30 lines. An inline summary comment would help future readers. Not actionable — the individual comments are adequate.

Tests (19/20)

6 new tests in "heartbeat recovery" describe block:

# Test Scenario
1 re-registers instead of failing persistently Happy path: 404 → re-register → new ID heartbeats → no escalation
2 escalates repeated losses 404 every tick, registration always succeeds → escalation after 3 non-exempted ticks
3 counts failed re-registration toward escalation Registration 500 → counts, reports registration error not 404
4 times out hung re-registration FakeTime, hung registration → 3 aborts → escalation
5 recovery scoped to scheduled heartbeat Concurrency: direct caller starts next beat while scheduled one recovers → no false escalation
6 stop during pending re-registration stop() → no adoption, no log

Test infrastructure extensions (:308-368): recordingLogger now captures infos; scriptedHeartbeatFetch accepts heartbeatResponse/registrationResponse callbacks and tracks registrationAttempts(). Clean, backwards-compatible additions.

Minor gap: no test for 404 on the very first heartbeat after initial registration (extreme edge case). Code handles it correctly by construction (awaitingHeartbeatAfterReregistration starts false, so first recovery is always exempted).

Reliability & Security (15/15)

  • Abort signal threaded through re-registration; stop() semantics unchanged.
  • retryWithBackoff with maxAttempts: 1 bounds recovery to one attempt + timeout.
  • Failed re-registrations count toward escalation — a dead control plane still surfaces.
  • No new auth surface; re-registration reuses registerAgentPushRuntimeService with existing bearer auth.
  • service_key upsert idempotency is a server-side guarantee, correctly relied upon.

Maintainability (14/15)

  • isLostRegistrationHeartbeatFailure is a standalone, well-documented predicate.
  • Lifecycle object refactored from inline return to named const — minimal and necessary for closure mutation.
  • Commit history (6 commits) is clean and tells the story.
  • Minor: the awaitingHeartbeatAfterReregistration/recoveredHeartbeats interaction could benefit from a brief 3-line state-diagram comment near their declarations.

Scope & Docs (8/10)

  • PR body is excellent: root cause, scope, red→green evidence, issue link.
  • Two files touched, tightly scoped to the fix.
  • Existing behavior unchanged: per-tick 4xx non-retry, transient 5xx retry, stop() semantics, escalation threshold.
  • Gap (informational, noted in prior review feat: expose additional AI SDK core exports #12): PR body omits deployment gates and post-deploy Sentry verification plan per the issue acceptance criteria.

Local verification

$ PATH=/Users/kentarowakayama/.deno-2.7.7/bin:$PATH deno check src/agent/service/registration.ts
Check src/agent/service/registration.ts   ✅

$ PATH=/Users/kentarowakayama/.deno-2.7.7/bin:$PATH deno task test:file src/agent/service/registration.test.ts
ok | 3 passed (27 steps) | 0 failed (10s)   ✅

All 27 steps across 3 describe blocks pass. Typecheck clean.

CI status

Typecheck, format, test-layout, sentry runtime packages, rsc browser e2e, proxy binary, npm smoke — all pass. Coverage shards, lint, integration, binary e2e — still pending at review time.


Review-Gate:
Reviewer: Claude
Reviewed-SHA: d5adcaa
Score: 94/100
Actionable-Findings: 0
Verdict: APPROVE

@kwakayama

Copy link
Copy Markdown
Contributor Author

Codex independent review for exact head d5adcaafc4ee5102aaf7a0be7fdc3964adab458c.

Findings: None.

Assessment:

  • Correctness: The recovery trigger is restricted to the existing NETWORK_ERROR classification with upstream HTTP 404. Re-registration reuses the stable service key, adopts the returned service row before later heartbeats, exempts only the recovery heartbeat, and lets repeated lost registrations or failed recovery reach the existing escalation.
  • Reliability and security: Recovery registration has a bounded attempt timeout composed with teardown cancellation. State publication is guarded after stop(), and the change adds no new secret or external-data handling.
  • Scope and maintainability: The exact three-dot diff is limited to src/agent/service/registration.ts and its colocated tests. The current PR body links issue fix(runtime): persist provider-executed streamed tool results #873 and documents deployment gates plus post-deploy Sentry verification.

Verification:

  • PATH=/Users/kentarowakayama/.deno-2.7.7/bin:$PATH deno task test:file src/agent/service/registration.test.ts: 3 passed, 27 steps, 0 failed.
  • PATH=/Users/kentarowakayama/.deno-2.7.7/bin:$PATH deno task test:file src/agent/service/: 58 passed, 150 steps, 0 failed.
  • deno lint, deno check, deno fmt --check, and git diff --check for the changed files and exact base-to-head diff: passed.
  • Review-state pagination is complete: 8 of 8 review threads returned, all resolved; 17 of 17 issue comments returned; no thread has more than 20 comments. The head remained stable during the gate snapshot.
  • Exact-head GitHub checks at the snapshot: 17 passed, 0 failed, 17 pending. Pending checks were not awaited per the request, so CI remains a merge-gate verification gap.
  • Skipped checks recorded by the snapshot: invalidate unverified review proof, close unresolved merge group review gate, reuse exact-head review for merge group, fail timed-out automated review, signal review reconciliation, find timed-out automated reviews, tests (split mode), version-check, and build-binaries.

Verdict: APPROVE for code review. Merge still requires the pending exact-head checks and repository review-gate conditions to complete.

Score: 95/100

Review-Gate:
Reviewer: Codex
Reviewed-SHA: d5adcaa
Score: 95/100
Actionable-Findings: 0
Verdict: APPROVE

@sonarqubecloud

Copy link
Copy Markdown

@kwakayama
kwakayama added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit fc251e7 Aug 31, 2026
63 checks passed
@kwakayama
kwakayama deleted the issue/873-heartbeat-persistent-failure branch August 31, 2026 07:23
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.

1 participant