~cytrogen/gstack

4e31acbd476ef5b50a68f1d7b7cc9c844c4710c8 — Garry Tan a month ago 43fbe16
fix: auto-clear stale heartbeat when process is dead

Add PID to heartbeat file. eval-watch checks process.kill(pid, 0) and
auto-deletes the heartbeat when the PID is no longer alive — no manual
cleanup needed after crashed/killed E2E runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 files changed, 21 insertions(+), 1 deletions(-)

M scripts/eval-watch.ts
M test/helpers/session-runner.ts
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',