-
Notifications
You must be signed in to change notification settings - Fork 34
feat(registry): auto-mark stale agents inactive via dynamic TTL windows #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid a shared default Defaulting to 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||
| * 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n "setInterval|axios\\.post|timeout|inFlight|encodeURIComponent" packages/agent_shared_directory/src/server.tsRepository: clevercon-protocol/clevercon Length of output: 204 🏁 Script executed: cat -n packages/agent_shared_directory/src/server.ts | head -30Repository: clevercon-protocol/clevercon Length of output: 1265 🏁 Script executed: rg -n "AGENT_ID" packages/agent_shared_directory/src/server.ts | head -5Repository: 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 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Structural Cleanup Management: Clear event pools if process triggers termination signals | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.on('SIGTERM', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| clearInterval(heartbeatInterval); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.on('SIGINT', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| clearInterval(heartbeatInterval); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate TTL and timestamp inputs before stale-state evaluation. Current parsing allows invalid behavior: negative TTL values are accepted, 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| 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. */ | ||||||||||||||||||||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove duplicated env keys to avoid ambiguous source-of-truth.
REGISTRY_URL(and likelyPORT) now appears in multiple sections. Even with identical values today, duplication is fragile and can drift.Suggested fix
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 144-144: [DuplicatedKey] The REGISTRY_URL key is duplicated
(DuplicatedKey)
🤖 Prompt for AI Agents
Source: Linters/SAST tools