feat: redesign health check to triangulate projects.json, issue label, and session state (#143) (#145)

## Changes

- Remove `activeSessions` parameter from health check (was never populated)
- Add gateway session lookup via `openclaw gateway call status`
- Add issue label lookup via `provider.getIssue(issueId)`
- Implement detection matrix with 6 issue types:
  - session_dead: active worker but session missing in gateway
  - label_mismatch: active worker but issue not in Doing/Testing
  - stale_worker: active for >2h
  - stuck_label: inactive but issue has Doing/Testing label
  - orphan_issue_id: inactive but issueId set
  - issue_gone: active but issue deleted/closed

## Files

- lib/services/health.ts — complete rewrite with three-source triangulation
- lib/tools/health.ts — remove activeSessions param, fetch sessions from gateway
- lib/services/heartbeat.ts — remove empty activeSessions calls, pass sessions map
This commit is contained in:
Lauren ten Hoor
2026-02-13 16:20:21 +08:00
committed by GitHub
parent 4a029c1b3b
commit 825c5e6f50
3 changed files with 337 additions and 68 deletions

View File

@@ -15,7 +15,7 @@ import fs from "node:fs";
import path from "node:path";
import { readProjects } from "../projects.js";
import { log as auditLog } from "../audit.js";
import { checkWorkerHealth } from "./health.js";
import { checkWorkerHealth, fetchGatewaySessions, type SessionLookup } from "./health.js";
import { projectTick } from "./tick.js";
import { createProvider } from "../providers/index.js";
import { notifyTickPickups, getNotificationConfig } from "../notify.js";
@@ -184,12 +184,16 @@ async function processAllAgents(
totalSkipped: 0,
};
// Fetch gateway sessions once for all agents/projects
const sessions = await fetchGatewaySessions();
for (const { agentId, workspace } of agents) {
const agentResult = await tick({
workspaceDir: workspace,
agentId,
config,
pluginConfig,
sessions,
logger,
});
@@ -221,9 +225,10 @@ export async function tick(opts: {
agentId?: string;
config: HeartbeatConfig;
pluginConfig?: Record<string, unknown>;
sessions: SessionLookup;
logger: { info(msg: string): void; warn(msg: string): void };
}): Promise<TickResult> {
const { workspaceDir, agentId, config, pluginConfig } = opts;
const { workspaceDir, agentId, config, pluginConfig, sessions } = opts;
const data = await readProjects(workspaceDir);
const projectIds = Object.keys(data.projects);
@@ -250,6 +255,7 @@ export async function tick(opts: {
workspaceDir,
groupId,
project,
sessions,
);
// Budget check: stop if we've hit the limit
@@ -304,6 +310,7 @@ async function performHealthPass(
workspaceDir: string,
groupId: string,
project: any,
sessions: SessionLookup,
): Promise<number> {
const { provider } = await createProvider({ repo: project.repo });
let fixedCount = 0;
@@ -314,7 +321,7 @@ async function performHealthPass(
groupId,
project,
role,
activeSessions: [],
sessions,
autoFix: true,
provider,
});
@@ -332,5 +339,3 @@ async function checkProjectActive(workspaceDir: string, groupId: string): Promis
if (!fresh) return false;
return fresh.dev.active || fresh.qa.active;
}