M scripts/eval-watch.ts => scripts/eval-watch.ts +20 -1
@@ 19,6 19,7 @@ const STALE_THRESHOLD_SEC = 600; // 10 minutes
export interface HeartbeatData {
runId: string;
+ pid?: number;
startedAt: string;
currentTest: string;
status: string;
@@ 51,6 52,16 @@ function readJSON<T>(filePath: string): T | null {
}
}
+/** Check if a process is alive (signal 0 = existence check, doesn't kill). */
+function isProcessAlive(pid: number): boolean {
+ try {
+ process.kill(pid, 0);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
/** Format seconds as Xm Ys */
function formatDuration(sec: number): string {
if (sec < 60) return `${sec}s`;
@@ 127,9 138,17 @@ if (import.meta.main) {
const showTail = process.argv.includes('--tail');
const render = () => {
- const heartbeat = readJSON<HeartbeatData>(HEARTBEAT_PATH);
+ let heartbeat = readJSON<HeartbeatData>(HEARTBEAT_PATH);
const partial = readJSON<PartialData>(PARTIAL_PATH);
+ // Auto-clear heartbeat if the process is dead
+ if (heartbeat?.pid && !isProcessAlive(heartbeat.pid)) {
+ try { fs.unlinkSync(HEARTBEAT_PATH); } catch { /* already gone */ }
+ process.stdout.write('\x1B[2J\x1B[H');
+ process.stdout.write(`Cleared stale heartbeat — PID ${heartbeat.pid} is no longer running.\n\n`);
+ heartbeat = null;
+ }
+
// Clear screen
process.stdout.write('\x1B[2J\x1B[H');
process.stdout.write(renderDashboard(heartbeat, partial) + '\n');
M test/helpers/session-runner.ts => test/helpers/session-runner.ts +1 -0
@@ 216,6 216,7 @@ export async function runSkillTest(options: {
const toolDesc = `${item.name}(${truncate(JSON.stringify(item.input || {}), 60)})`;
atomicWriteSync(HEARTBEAT_PATH, JSON.stringify({
runId,
+ pid: proc.pid,
startedAt,
currentTest: testName,
status: 'running',