Skip to content

Commit a5e34b8

Browse files
ericjutacodex
andcommitted
fix: filter ghost workers from health output
Co-authored-by: Codex <noreply@openai.com>
1 parent 4e1c615 commit a5e34b8

4 files changed

Lines changed: 97 additions & 3 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,12 +959,22 @@ This restarts both the engine and worker, clearing stale channels. Data is prese
959959
# Restart without rebuilding (clears stale engine state)
960960
docker compose restart
961961

962+
# Rebuild the worker image explicitly
963+
docker compose build agentmemory-worker
964+
962965
# Rebuild after code changes
963966
docker compose up -d --build
964967

965968
# Rebuild only the worker (faster, keeps engine running)
966969
docker compose up -d --build agentmemory-worker
967970

971+
# Force-recreate the worker from the current local image
972+
docker compose up -d --force-recreate agentmemory-worker
973+
974+
# Common live-fix sequence after worker code changes
975+
docker compose build agentmemory-worker
976+
docker compose up -d --force-recreate agentmemory-worker
977+
968978
# View worker logs
969979
docker compose logs -f agentmemory-worker
970980

src/health/monitor.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type { ISdk } from "iii-sdk";
33
import { getHeapStatistics } from "node:v8";
44
import type { HealthSnapshot } from "../types.js";
5+
import type { HealthWorker } from "../types.js";
56
import type { StateKV } from "../state/kv.js";
67
import { KV } from "../state/schema.js";
78
import { evaluateHealth } from "./thresholds.js";
@@ -12,12 +13,51 @@ let latestHealthSnapshot: HealthSnapshot | null = null;
1213
const BASE_INTERVAL_MS = 30_000;
1314
const MAX_INTERVAL_MS = 300_000; // 5 min cap when backing off
1415

16+
const GHOST_WORKER_CONNECTED_DELTA_MS = 5_000;
17+
1518
export interface PipelineMetrics {
1619
compressActive: number;
1720
compressPending: number;
1821
totalInflight: number;
1922
}
2023

24+
function isGhostWorkerCandidate(worker: HealthWorker): boolean {
25+
return (
26+
worker.status === "connected" &&
27+
(worker.function_count ?? 0) === 0 &&
28+
!worker.name &&
29+
!worker.runtime &&
30+
!worker.version
31+
);
32+
}
33+
34+
function sharesWorkerOrigin(a: HealthWorker, b: HealthWorker): boolean {
35+
if (a.ip_address && b.ip_address && a.ip_address === b.ip_address) {
36+
return true;
37+
}
38+
if (
39+
typeof a.connected_at_ms === "number" &&
40+
typeof b.connected_at_ms === "number" &&
41+
Math.abs(a.connected_at_ms - b.connected_at_ms) <= GHOST_WORKER_CONNECTED_DELTA_MS
42+
) {
43+
return true;
44+
}
45+
return false;
46+
}
47+
48+
export function filterGhostWorkers(workers: HealthWorker[]): HealthWorker[] {
49+
return workers.filter((worker) => {
50+
if (!isGhostWorkerCandidate(worker)) return true;
51+
return !workers.some((other) => {
52+
if (other.id === worker.id) return false;
53+
if (other.status !== "connected") return false;
54+
if ((other.function_count ?? 0) <= 0) return false;
55+
if (!other.name) return false;
56+
return sharesWorkerOrigin(worker, other);
57+
});
58+
});
59+
}
60+
2161
export function registerHealthMonitor(
2262
sdk: ISdk,
2363
kv: StateKV,
@@ -75,7 +115,7 @@ export function registerHealthMonitor(
75115
unknown,
76116
{ workers?: HealthSnapshot["workers"] }
77117
>({ function_id: "engine::workers::list", payload: {} });
78-
if (result?.workers) workers = result.workers;
118+
if (result?.workers) workers = filterGhostWorkers(result.workers);
79119
} catch {}
80120

81121
// Skip KV probe when persist circuit is open — engine is already struggling

src/types.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,9 +348,21 @@ export interface FunctionMetrics {
348348
avgQualityScore: number;
349349
}
350350

351+
export interface HealthWorker {
352+
id: string;
353+
name: string | null;
354+
status: string;
355+
connected_at_ms?: number;
356+
function_count?: number;
357+
ip_address?: string | null;
358+
runtime?: string | null;
359+
version?: string | null;
360+
[key: string]: unknown;
361+
}
362+
351363
export interface HealthSnapshot {
352364
connectionState: string;
353-
workers: Array<{ id: string; name: string; status: string }>;
365+
workers: HealthWorker[];
354366
memory: {
355367
heapUsed: number;
356368
heapTotal: number;

test/health-monitor.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22

3-
import { registerHealthMonitor, getLatestHealth } from "../src/health/monitor.js";
3+
import {
4+
filterGhostWorkers,
5+
registerHealthMonitor,
6+
getLatestHealth,
7+
} from "../src/health/monitor.js";
48
import { KV } from "../src/state/schema.js";
59

610
describe("registerHealthMonitor", () => {
@@ -46,4 +50,32 @@ describe("registerHealthMonitor", () => {
4650

4751
monitor.stop();
4852
});
53+
54+
it("filters the duplicate anonymous zero-function ghost worker", () => {
55+
const workers = filterGhostWorkers([
56+
{
57+
id: "real",
58+
name: "agentmemory",
59+
status: "connected",
60+
function_count: 289,
61+
connected_at_ms: 1001,
62+
ip_address: "172.18.0.3",
63+
runtime: "node",
64+
version: "0.11.0",
65+
},
66+
{
67+
id: "ghost",
68+
name: null,
69+
status: "connected",
70+
function_count: 0,
71+
connected_at_ms: 1000,
72+
ip_address: "172.18.0.3",
73+
runtime: null,
74+
version: null,
75+
},
76+
]);
77+
78+
expect(workers).toHaveLength(1);
79+
expect(workers[0]?.id).toBe("real");
80+
});
4981
});

0 commit comments

Comments
 (0)