Skip to content

Commit c5f3c96

Browse files
gcmsgclaude
andcommitted
fix: preserve real last_heartbeat when marking agent offline
The stale agent checker was calling Heartbeat() to mark agents offline, which also updated last_heartbeat to the current time. This made agents appear to have a recent heartbeat while showing status=offline. Add SetStatus() method that updates only the status column without touching last_heartbeat, so the dashboard correctly shows when the agent last actually heartbeated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c3263f6 commit c5f3c96

File tree

5 files changed

+40
-1
lines changed

5 files changed

+40
-1
lines changed

cmd/peerclawd/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,8 @@ func main() {
480480
}
481481
for _, agentID := range staleAgents {
482482
// Mark offline first so next tick won't pick it up again.
483-
_, _ = regService.Heartbeat(ctx, agentID, agentcard.StatusOffline, nil)
483+
// Use SetStatus (not Heartbeat) to preserve the real last_heartbeat timestamp.
484+
_ = regService.SetStatus(ctx, agentID, agentcard.StatusOffline)
484485

485486
_ = repEngine.RecordEvent(ctx, agentID, "heartbeat_miss", "")
486487
logger.Info("agent marked offline due to heartbeat timeout", "agent_id", agentID)

internal/registry/postgres.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,21 @@ func (s *PostgresStore) UpdateHeartbeat(ctx context.Context, id string, status a
255255
return nil
256256
}
257257

258+
func (s *PostgresStore) UpdateStatus(ctx context.Context, id string, status agentcard.AgentStatus) error {
259+
res, err := s.db.ExecContext(ctx,
260+
"UPDATE agents SET status = $1 WHERE id = $2",
261+
string(status), id,
262+
)
263+
if err != nil {
264+
return err
265+
}
266+
n, _ := res.RowsAffected()
267+
if n == 0 {
268+
return fmt.Errorf("agent %s not found", id)
269+
}
270+
return nil
271+
}
272+
258273
func (s *PostgresStore) UpdateMetadata(ctx context.Context, id string, metadata map[string]string) error {
259274
if len(metadata) == 0 {
260275
return nil

internal/registry/service.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ func (s *Service) Heartbeat(ctx context.Context, agentID string, status agentcar
134134
return deadline, nil
135135
}
136136

137+
// SetStatus updates only the agent's status without touching last_heartbeat.
138+
func (s *Service) SetStatus(ctx context.Context, agentID string, status agentcard.AgentStatus) error {
139+
return s.store.UpdateStatus(ctx, agentID, status)
140+
}
141+
137142
// GetAgent retrieves a single agent by ID.
138143
func (s *Service) GetAgent(ctx context.Context, agentID string) (*agentcard.Card, error) {
139144
card, err := s.store.Get(ctx, agentID)

internal/registry/sqlite.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,21 @@ func (s *SQLiteStore) UpdateHeartbeat(ctx context.Context, id string, status age
281281
return nil
282282
}
283283

284+
func (s *SQLiteStore) UpdateStatus(ctx context.Context, id string, status agentcard.AgentStatus) error {
285+
res, err := s.db.ExecContext(ctx,
286+
"UPDATE agents SET status = ? WHERE id = ?",
287+
string(status), id,
288+
)
289+
if err != nil {
290+
return err
291+
}
292+
n, _ := res.RowsAffected()
293+
if n == 0 {
294+
return fmt.Errorf("agent %s not found", id)
295+
}
296+
return nil
297+
}
298+
284299
func (s *SQLiteStore) UpdateMetadata(ctx context.Context, id string, metadata map[string]string) error {
285300
if len(metadata) == 0 {
286301
return nil

internal/registry/store.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ type Store interface {
5555
// UpdateHeartbeat updates the heartbeat timestamp and status of an agent.
5656
UpdateHeartbeat(ctx context.Context, id string, status agentcard.AgentStatus) error
5757

58+
// UpdateStatus updates only the status of an agent without touching last_heartbeat.
59+
UpdateStatus(ctx context.Context, id string, status agentcard.AgentStatus) error
60+
5861
// UpdateMetadata merges the provided metadata keys into the agent's existing metadata.
5962
UpdateMetadata(ctx context.Context, id string, metadata map[string]string) error
6063

0 commit comments

Comments
 (0)