Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,11 @@ WEB_INTEL_SELF_URL=http://localhost:4002
WEB_INTEL_V2_SELF_URL=http://localhost:4003
ANALYSIS_AGENT_SELF_URL=http://localhost:4004
REPORT_AGENT_SELF_URL=http://localhost:4005

# Cluster Runtime Environments
PORT=3000
REGISTRY_URL=http://localhost:3000
Comment on lines +143 to +144

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


# Agent Cluster Core Policies
# Time-To-Live (TTL) ceiling boundary value in seconds to mark uncommunicative workers inactive.
AGENT_TTL_SECONDS=120
28 changes: 28 additions & 0 deletions packages/agent_shared_directory/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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.


/**
* Registers startup configuration and provisions automatic lifecycle loops
*/
export function initializeAgentHeartbeat() {
console.log(`[Lifecycle] Initializing background pulse telemetry for Agent: ${AGENT_ID}`);

// Establish heartbeat cycle matching the 60-second execution window specifications
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);
Comment on lines +13 to +19

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.


// Structural Cleanup Management: Clear event pools if process triggers termination signals
process.on('SIGTERM', () => {
clearInterval(heartbeatInterval);
});
process.on('SIGINT', () => {
clearInterval(heartbeatInterval);
});
}
43 changes: 37 additions & 6 deletions packages/registry/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,45 @@ function ensureDataDir(): void {
/**
* Load all registered agents from `data/registry.json`.
* Returns an empty array if the file doesn't exist or contains invalid JSON.
* * Process state conversions automatically to evaluate agent freshness.
*/
export function loadAgents(): AgentRecord[] {
export function loadAgents(includeInactive: boolean = false): AgentRecord[] {
ensureDataDir();
if (!fs.existsSync(REGISTRY_FILE)) return [];

let agents: AgentRecord[] = [];
try {
return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf-8'));
agents = JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf-8'));
} catch {
return [];
}

const nowMs = Date.now();
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;
Comment on lines +40 to +47

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.


if (isStale && agent.status === 'active') {
mutated = true;
return { ...agent, status: 'inactive' as const };
}
return agent;
});

// Automatically sync back to JSON storage file if any statuses collapsed to inactive
if (mutated) {
saveAgents(updatedAgents);
}

if (includeInactive) {
return updatedAgents;
}
return updatedAgents.filter((a) => a.status === 'active');
}

/** Overwrite `data/registry.json` with the given list of agents. */
Expand All @@ -42,15 +72,16 @@ export function saveAgents(agents: AgentRecord[]): void {

/** Find a single agent by its `agent_id`, or `undefined` if not registered. */
export function findAgent(agentId: string): AgentRecord | undefined {
return loadAgents().find((a) => a.agent_id === agentId);
// Pass true to verify matching references across inactive items as well
return loadAgents(true).find((a) => a.agent_id === agentId);
}

/**
* Insert a new agent or replace an existing one with the same `agent_id`.
* Returns the agent that was stored.
*/
export function upsertAgent(agent: AgentRecord): AgentRecord {
const agents = loadAgents();
const agents = loadAgents(true);
const idx = agents.findIndex((a) => a.agent_id === agent.agent_id);
if (idx >= 0) {
agents[idx] = agent;
Expand All @@ -63,9 +94,9 @@ export function upsertAgent(agent: AgentRecord): AgentRecord {

/** Remove an agent by `agent_id`. Returns `true` if an agent was removed. */
export function removeAgent(agentId: string): boolean {
const agents = loadAgents();
const agents = loadAgents(true);
const filtered = agents.filter((a) => a.agent_id !== agentId);
if (filtered.length === agents.length) return false;
saveAgents(filtered);
return true;
}
}
Loading