Skip to content

feat(registry): auto-mark stale agents inactive via dynamic TTL windows - #19

Open
mijinummi wants to merge 1 commit into
clevercon-protocol:mainfrom
mijinummi:feature/agent-heartbeat-ttl-12
Open

feat(registry): auto-mark stale agents inactive via dynamic TTL windows#19
mijinummi wants to merge 1 commit into
clevercon-protocol:mainfrom
mijinummi:feature/agent-heartbeat-ttl-12

Conversation

@mijinummi

@mijinummi mijinummi commented Jun 21, 2026

Copy link
Copy Markdown

Summary

  • Appends atomic POST /agents/:id/heartbeat keep-alive processing boundaries
  • Exposes include_inactive query controls inside GET /agents routing tracks
  • Implements background interval loop managers across internal agent daemons
  • Adds AGENT_TTL_SECONDS parameter boundaries to .env configurations
  • Closes [Task]: Auto-mark stale agents inactive when last_seen exceeds TTL #12"

Summary by CodeRabbit

  • New Features
    • Agents now send periodic heartbeat updates to the registry
    • Agents are automatically marked as inactive after exceeding the configured time-to-live duration
    • New environment variable added for configuring agent timeout settings in cluster deployments

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds TTL-based stale agent detection to the registry store: loadAgents now marks agents with last_seen older than AGENT_TTL_SECONDS as inactive, persists changes, and optionally filters them out. All store CRUD operations are updated to include inactive agents. A new initializeAgentHeartbeat() function sends periodic POSTs to the registry heartbeat endpoint and handles process shutdown. .env.example documents the new AGENT_TTL_SECONDS setting.

Changes

Agent Heartbeat and Registry TTL Staleness

Layer / File(s) Summary
Registry store: TTL staleness, inactive marking, and CRUD updates
packages/registry/src/store.ts
loadAgents gains an includeInactive boolean parameter, computes agent age from last_seen against AGENT_TTL_SECONDS, converts stale active agents to inactive, and calls saveAgents when any conversions occur. Returns only active agents when includeInactive is false. findAgent, upsertAgent, and removeAgent each switch to loadAgents(true) so they operate over active and inactive agents alike.
Agent heartbeat client and env config
packages/agent_shared_directory/src/server.ts, .env.example
Introduces initializeAgentHeartbeat(), which reads REGISTRY_URL and AGENT_ID from env with defaults, starts a 60-second setInterval that POSTs to /agents/:id/heartbeat, logs warnings on failure, and clears the interval on SIGTERM/SIGINT. .env.example adds AGENT_TTL_SECONDS=120 and cluster section headers alongside duplicated PORT and REGISTRY_URL defaults.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 Thump thump, every minute I send my beat,
To tell the registry my heart's complete.
If silence falls and the clock runs past,
The stale are marked — inactive at last.
No phantom agents shall clog the plan,
For the bunny watches as only bunnies can! 🕐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR partially implements the requirements: adds heartbeat loop initialization and TTL-based filtering, but the POST heartbeat endpoint and include_inactive query parameter are missing from the provided changes. Implement the missing POST /agents/:id/heartbeat endpoint in packages/registry/src/server.ts and add include_inactive query parameter support to GET /agents handler.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: auto-marking stale agents as inactive using a dynamic TTL mechanism, which is the core functionality implemented.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing agent heartbeat and TTL functionality as specified in issue #12; no out-of-scope modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Around line 143-144: Remove the duplicate environment variable definitions
from the .env.example file. The `PORT` and `REGISTRY_URL` variables are defined
in multiple sections, creating ambiguity about which is the authoritative
source. Consolidate these variables into a single location in the file, keeping
only one definition of each environment variable to maintain a clear and
unambiguous source of truth that cannot drift over time.

In `@packages/agent_shared_directory/src/server.ts`:
- Line 4: The AGENT_ID constant currently has a default fallback value of
'agent-core-01' which can cause multiple daemon instances to share the same
identity if the environment variable is not properly configured. Remove the
default fallback value from the AGENT_ID assignment on that line and add error
handling to ensure the AGENT_ID environment variable is explicitly set, throwing
an error or exiting the process if it is missing or undefined.
- Around line 13-19: The heartbeat loop in the setInterval callback lacks
timeout protection and overlap prevention. Add a timeout option to the
axios.post call (e.g., using the timeout configuration) and introduce a boolean
flag outside the setInterval to track whether a heartbeat request is currently
in-flight. Before making the axios.post request, check this flag and skip the
request if one is already pending, then set the flag to true before the request
and false in both the try and catch blocks to prevent overlapping requests.
Additionally, URL-encode the AGENT_ID using encodeURIComponent when constructing
the URL to handle special characters safely.

In `@packages/registry/src/store.ts`:
- Around line 40-47: The TTL and timestamp validation logic lacks input
validation which allows incorrect staleness evaluation. First, validate the
parsed ttlSeconds value to ensure it is a positive number greater than zero
before using it to calculate ttlThresholdMs, rejecting zero or negative values.
Second, when parsing the agent.last_seen timestamp in the agents.map function,
validate that the resulting lastSeenMs is a valid number (not NaN) before
calculating isStale, and skip or handle agents with invalid timestamps
appropriately to prevent them from being incorrectly classified as active. This
ensures only valid TTL and timestamp values are used in the stale-state
evaluation logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 682aad20-345a-4bef-bea1-2e68f5123be1

📥 Commits

Reviewing files that changed from the base of the PR and between 66c5d4c and 567b08e.

📒 Files selected for processing (3)
  • .env.example
  • packages/agent_shared_directory/src/server.ts
  • packages/registry/src/store.ts

Comment thread .env.example
Comment on lines +143 to +144
PORT=3000
REGISTRY_URL=http://localhost:3000

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove duplicated env keys to avoid ambiguous source-of-truth.

REGISTRY_URL (and likely PORT) now appears in multiple sections. Even with identical values today, duplication is fragile and can drift.

Suggested fix
-# Cluster Runtime Environments
-PORT=3000
-REGISTRY_URL=http://localhost:3000
-
 # Agent Cluster Core Policies
 # Time-To-Live (TTL) ceiling boundary value in seconds to mark uncommunicative workers inactive.
 AGENT_TTL_SECONDS=120
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 144-144: [DuplicatedKey] The REGISTRY_URL key is duplicated

(DuplicatedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example around lines 143 - 144, Remove the duplicate environment
variable definitions from the .env.example file. The `PORT` and `REGISTRY_URL`
variables are defined in multiple sections, creating ambiguity about which is
the authoritative source. Consolidate these variables into a single location in
the file, keeping only one definition of each environment variable to maintain a
clear and unambiguous source of truth that cannot drift over time.

Source: Linters/SAST tools

import axios from 'axios';

const REGISTRY_URL = process.env.REGISTRY_URL || 'http://localhost:3000';
const AGENT_ID = process.env.AGENT_ID || 'agent-core-01';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid a shared default AGENT_ID; require explicit identity.

Defaulting to agent-core-01 risks multiple daemons heartbeating under the same registry record if env wiring is missed.

Suggested fix
-const AGENT_ID = process.env.AGENT_ID || 'agent-core-01';
+const AGENT_ID = process.env.AGENT_ID;
+if (!AGENT_ID) {
+  throw new Error('AGENT_ID is required for heartbeat initialization');
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const AGENT_ID = process.env.AGENT_ID || 'agent-core-01';
const AGENT_ID = process.env.AGENT_ID;
if (!AGENT_ID) {
throw new Error('AGENT_ID is required for heartbeat initialization');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent_shared_directory/src/server.ts` at line 4, The AGENT_ID
constant currently has a default fallback value of 'agent-core-01' which can
cause multiple daemon instances to share the same identity if the environment
variable is not properly configured. Remove the default fallback value from the
AGENT_ID assignment on that line and add error handling to ensure the AGENT_ID
environment variable is explicitly set, throwing an error or exiting the process
if it is missing or undefined.

Comment on lines +13 to +19
const heartbeatInterval = setInterval(async () => {
try {
await axios.post(`${REGISTRY_URL}/agents/${AGENT_ID}/heartbeat`);
} catch (error: any) {
console.warn(`[Lifecycle Warning] Pulse telemetry transmission dropped: ${error.message}`);
}
}, 60000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "setInterval|axios\\.post|timeout|inFlight|encodeURIComponent" packages/agent_shared_directory/src/server.ts

Repository: clevercon-protocol/clevercon

Length of output: 204


🏁 Script executed:

cat -n packages/agent_shared_directory/src/server.ts | head -30

Repository: clevercon-protocol/clevercon

Length of output: 1265


🏁 Script executed:

rg -n "AGENT_ID" packages/agent_shared_directory/src/server.ts | head -5

Repository: clevercon-protocol/clevercon

Length of output: 303


Add timeout and overlap protection to the heartbeat loop.

The heartbeat mechanism has no protection against hung network calls or request overlap. If an axios.post call takes longer than the 60-second interval, subsequent intervals will fire while the previous request is still in-flight, causing overlapping requests and potential resource exhaustion. Additionally, the AGENT_ID is not URL-encoded, which could cause issues if it contains special characters.

Suggested fix
-  const heartbeatInterval = setInterval(async () => {
+  let inFlight = false;
+  const heartbeatInterval = setInterval(async () => {
+    if (inFlight) return;
+    inFlight = true;
     try {
-      await axios.post(`${REGISTRY_URL}/agents/${AGENT_ID}/heartbeat`);
+      await axios.post(
+        `${REGISTRY_URL}/agents/${encodeURIComponent(AGENT_ID)}/heartbeat`,
+        undefined,
+        { timeout: 5000 }
+      );
     } catch (error: any) {
       console.warn(`[Lifecycle Warning] Pulse telemetry transmission dropped: ${error.message}`);
+    } finally {
+      inFlight = false;
     }
   }, 60000);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const heartbeatInterval = setInterval(async () => {
try {
await axios.post(`${REGISTRY_URL}/agents/${AGENT_ID}/heartbeat`);
} catch (error: any) {
console.warn(`[Lifecycle Warning] Pulse telemetry transmission dropped: ${error.message}`);
}
}, 60000);
let inFlight = false;
const heartbeatInterval = setInterval(async () => {
if (inFlight) return;
inFlight = true;
try {
await axios.post(
`${REGISTRY_URL}/agents/${encodeURIComponent(AGENT_ID)}/heartbeat`,
undefined,
{ timeout: 5000 }
);
} catch (error: any) {
console.warn(`[Lifecycle Warning] Pulse telemetry transmission dropped: ${error.message}`);
} finally {
inFlight = false;
}
}, 60000);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/agent_shared_directory/src/server.ts` around lines 13 - 19, The
heartbeat loop in the setInterval callback lacks timeout protection and overlap
prevention. Add a timeout option to the axios.post call (e.g., using the timeout
configuration) and introduce a boolean flag outside the setInterval to track
whether a heartbeat request is currently in-flight. Before making the axios.post
request, check this flag and skip the request if one is already pending, then
set the flag to true before the request and false in both the try and catch
blocks to prevent overlapping requests. Additionally, URL-encode the AGENT_ID
using encodeURIComponent when constructing the URL to handle special characters
safely.

Comment on lines +40 to +47
const ttlSeconds = Number(process.env.AGENT_TTL_SECONDS) || 120;
const ttlThresholdMs = ttlSeconds * 1000;
let mutated = false;

// Process live lifecycle updates based on absolute timestamp drift
const updatedAgents = agents.map((agent) => {
const lastSeenMs = new Date(agent.last_seen).getTime();
const isStale = nowMs - lastSeenMs > ttlThresholdMs;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate TTL and timestamp inputs before stale-state evaluation.

Current parsing allows invalid behavior: negative TTL values are accepted, 0 is silently replaced by 120, and malformed last_seen parses to NaN (so records never become stale). This can misclassify agent activity.

Suggested fix
-  const ttlSeconds = Number(process.env.AGENT_TTL_SECONDS) || 120;
+  const parsedTtl = Number(process.env.AGENT_TTL_SECONDS);
+  const ttlSeconds = Number.isFinite(parsedTtl) && parsedTtl > 0 ? parsedTtl : 120;
   const ttlThresholdMs = ttlSeconds * 1000;
@@
-    const lastSeenMs = new Date(agent.last_seen).getTime();
-    const isStale = nowMs - lastSeenMs > ttlThresholdMs;
+    const lastSeenMs = Date.parse(agent.last_seen);
+    const isStale = Number.isNaN(lastSeenMs) || nowMs - lastSeenMs > ttlThresholdMs;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ttlSeconds = Number(process.env.AGENT_TTL_SECONDS) || 120;
const ttlThresholdMs = ttlSeconds * 1000;
let mutated = false;
// Process live lifecycle updates based on absolute timestamp drift
const updatedAgents = agents.map((agent) => {
const lastSeenMs = new Date(agent.last_seen).getTime();
const isStale = nowMs - lastSeenMs > ttlThresholdMs;
const parsedTtl = Number(process.env.AGENT_TTL_SECONDS);
const ttlSeconds = Number.isFinite(parsedTtl) && parsedTtl > 0 ? parsedTtl : 120;
const ttlThresholdMs = ttlSeconds * 1000;
let mutated = false;
// Process live lifecycle updates based on absolute timestamp drift
const updatedAgents = agents.map((agent) => {
const lastSeenMs = Date.parse(agent.last_seen);
const isStale = Number.isNaN(lastSeenMs) || nowMs - lastSeenMs > ttlThresholdMs;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/registry/src/store.ts` around lines 40 - 47, The TTL and timestamp
validation logic lacks input validation which allows incorrect staleness
evaluation. First, validate the parsed ttlSeconds value to ensure it is a
positive number greater than zero before using it to calculate ttlThresholdMs,
rejecting zero or negative values. Second, when parsing the agent.last_seen
timestamp in the agents.map function, validate that the resulting lastSeenMs is
a valid number (not NaN) before calculating isStale, and skip or handle agents
with invalid timestamps appropriately to prevent them from being incorrectly
classified as active. This ensures only valid TTL and timestamp values are used
in the stale-state evaluation logic.

@Bosun-Josh121

Copy link
Copy Markdown
Collaborator

@mijinummi pls fix failed CI and address major comments from coderabbit. Nice work

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.

[Task]: Auto-mark stale agents inactive when last_seen exceeds TTL

2 participants