From 6c9ce7aaed678cc4034eb9ff1b200b89a936b772 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 26 May 2026 18:33:07 -0700 Subject: [PATCH 1/4] feat(cli): add hidden --fun beacon buddy easter egg ASCII pet that reacts to beacon node state each slot. Opt-in via hidden --fun off|tty|file|both flag (default off; bare --fun resolves to both). Read-only telemetry: peer count, sync status, fork name, chain reorgs. Mood machine: happy / sleepy / panic / sad plus rare sprites for slot 1337, 31337, every millionth slot, and fork activation slots. TTY mode pins the frame to the bottom of the terminal using a DECSTBM scroll region so concurrent logger output keeps flowing above; downgrades to sidecar-only when stdout is not a TTY or the terminal is too small. File mode writes /buddy.txt once per slot (tail -F or watch -n1 cat to follow). AI-assisted: drafted with Claude Opus 4.7. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/beacon-node/src/util/index.ts | 1 + packages/cli/src/cmds/beacon/handler.ts | 8 ++ packages/cli/src/cmds/beacon/options.ts | 14 +++ packages/cli/src/util/buddy/buddy.ts | 55 ++++++++++ packages/cli/src/util/buddy/index.ts | 101 +++++++++++++++++++ packages/cli/src/util/buddy/render.ts | 117 ++++++++++++++++++++++ packages/cli/src/util/buddy/sprites.ts | 32 ++++++ packages/cli/src/util/buddy/types.ts | 28 ++++++ packages/cli/test/unit/util/buddy.test.ts | 88 ++++++++++++++++ 9 files changed, 444 insertions(+) create mode 100644 packages/cli/src/util/buddy/buddy.ts create mode 100644 packages/cli/src/util/buddy/index.ts create mode 100644 packages/cli/src/util/buddy/render.ts create mode 100644 packages/cli/src/util/buddy/sprites.ts create mode 100644 packages/cli/src/util/buddy/types.ts create mode 100644 packages/cli/test/unit/util/buddy.test.ts diff --git a/packages/beacon-node/src/util/index.ts b/packages/beacon-node/src/util/index.ts index 0aa95e173007..8acff4996c05 100644 --- a/packages/beacon-node/src/util/index.ts +++ b/packages/beacon-node/src/util/index.ts @@ -1,2 +1,3 @@ // Only export the functions which are useful for external packages and difficult to reproduce or mock export * from "./kzg.js"; +export {ClockEvent} from "./clock.js"; diff --git a/packages/cli/src/cmds/beacon/handler.ts b/packages/cli/src/cmds/beacon/handler.ts index e329bcfef4d6..40ab8e58c76e 100644 --- a/packages/cli/src/cmds/beacon/handler.ts +++ b/packages/cli/src/cmds/beacon/handler.ts @@ -14,6 +14,7 @@ import {BeaconNodeOptions, getBeaconConfigFromArgs} from "../../config/index.js" import {getNetworkBootnodes, isKnownNetworkName, readBootnodes} from "../../networks/index.js"; import {GlobalArgs, parseBeaconNodeArgs} from "../../options/index.js"; import {LogArgs} from "../../options/logOptions.js"; +import {startBeaconBuddy} from "../../util/buddy/index.js"; import { cleanOldLogFiles, mkdir, @@ -104,6 +105,11 @@ export async function beaconHandler(args: BeaconArgs & GlobalArgs): Promise = { @@ -169,6 +170,19 @@ export const beaconExtraOptions: CliCommandOptions = { description: "Disable light client server.", type: "boolean", }, + + fun: { + hidden: true, + description: "Enable beacon buddy ASCII pet easter egg (off|tty|file|both)", + type: "string", + choices: ["off", "tty", "file", "both"], + default: "off", + coerce: (v: string | boolean | undefined): "off" | "tty" | "file" | "both" => { + // Bare `--fun` (no value) resolves to "both" + if (v === true || v === "" || v === undefined) return "both"; + return v as "off" | "tty" | "file" | "both"; + }, + }, }; type ENRArgs = { diff --git a/packages/cli/src/util/buddy/buddy.ts b/packages/cli/src/util/buddy/buddy.ts new file mode 100644 index 000000000000..be57f67bb478 --- /dev/null +++ b/packages/cli/src/util/buddy/buddy.ts @@ -0,0 +1,55 @@ +import {BuddyState, Mood, RareSprite, SpriteKind} from "./types.js"; + +const SLOTS_PER_EPOCH = 32; +const REORG_LOOKBACK_SLOTS = 32; +const SYNC_LAG_THRESHOLD_SLOTS = 32; +const RARE_SLOTS = new Set([1337, 31337]); +const MILLION = 1_000_000; + +export type Telemetry = { + slot: number; + peers: number; + isSyncing: boolean; + syncDistance: number; + fork: string; + prevFork: string; + lastReorgSlot: number | null; +}; + +export function computeMood(t: Telemetry): Mood { + if (t.peers === 0) return Mood.panic; + if (t.lastReorgSlot !== null && t.slot - t.lastReorgSlot <= REORG_LOOKBACK_SLOTS) return Mood.sad; + if (t.isSyncing && t.syncDistance > SYNC_LAG_THRESHOLD_SLOTS) return Mood.sleepy; + return Mood.happy; +} + +export function computeRare(t: Telemetry): {rare: RareSprite; label?: string} | null { + if (RARE_SLOTS.has(t.slot)) { + return {rare: t.slot === 1337 ? RareSprite.slot1337 : RareSprite.slot31337, label: `slot ${t.slot}`}; + } + if (t.slot >= MILLION && t.slot % MILLION === 0) { + return {rare: RareSprite.slotMillion, label: `${t.slot / MILLION}M`}; + } + if (t.fork !== t.prevFork) { + return {rare: RareSprite.forkActivation, label: t.fork}; + } + return null; +} + +export function buildState(t: Telemetry): BuddyState { + return { + slot: t.slot, + epoch: Math.floor(t.slot / SLOTS_PER_EPOCH), + fork: t.fork, + peers: t.peers, + synced: !t.isSyncing, + syncDistance: t.syncDistance, + mood: computeMood(t), + override: computeRare(t) ?? undefined, + }; +} + +export function spriteKindFor(state: BuddyState): SpriteKind { + if (state.override) return {kind: "rare", rare: state.override.rare, label: state.override.label}; + return {kind: "mood", mood: state.mood}; +} diff --git a/packages/cli/src/util/buddy/index.ts b/packages/cli/src/util/buddy/index.ts new file mode 100644 index 000000000000..74c4acb21dbb --- /dev/null +++ b/packages/cli/src/util/buddy/index.ts @@ -0,0 +1,101 @@ +import path from "node:path"; +import {routes} from "@lodestar/api"; +import type {BeaconNode} from "@lodestar/beacon-node"; +import {ClockEvent} from "@lodestar/beacon-node/util"; +import type {Logger} from "@lodestar/logger"; +import {Telemetry, buildState, spriteKindFor} from "./buddy.js"; +import {TtyRenderer, formatFrame, writeSidecar} from "./render.js"; +import {renderSprite} from "./sprites.js"; +import {BuddyMode} from "./types.js"; + +export type StartBuddyOpts = { + node: BeaconNode; + dataDir: string; + mode: BuddyMode; + logger: Logger; +}; + +export type BuddyHandle = { + stop(): void; +}; + +export function startBeaconBuddy({node, dataDir, mode, logger}: StartBuddyOpts): BuddyHandle { + if (mode === "off") return {stop: () => {}}; + + // sidecar is rewritten every slot; `watch -n1 cat /buddy.txt` to follow + const sidecarPath = path.join(dataDir, "buddy.txt"); + let lastReorgSlot: number | null = null; + let prevFork = ""; + + const ttyWanted = mode === "tty" || mode === "both"; + let fileWanted = mode === "file" || mode === "both"; + let renderer: TtyRenderer | null = null; + if (ttyWanted) { + if (TtyRenderer.isSupported()) { + renderer = new TtyRenderer(); + } else { + // No TTY available: fall back to sidecar file so the buddy still works. + fileWanted = true; + logger.warn("beacon buddy: stdout is not a TTY (or too small); falling back to sidecar file only"); + } + } + + const onReorg = ({slot}: routes.events.EventData[routes.events.EventType.chainReorg]): void => { + lastReorgSlot = slot; + }; + + const onSlot = (slot: number): void => { + const peers = safeCall(() => node.network.getConnectedPeerCount(), 0); + const sync = safeCall(() => node.sync.getSyncStatus(), null); + const fork = node.chain.config.getForkName(slot); + if (prevFork === "") prevFork = fork; + + const telemetry: Telemetry = { + slot, + peers, + isSyncing: sync ? sync.isSyncing : false, + syncDistance: sync ? Number(sync.syncDistance) : 0, + fork, + prevFork, + lastReorgSlot, + }; + + const state = buildState(telemetry); + const sprite = renderSprite(spriteKindFor(state)); + const frame = formatFrame(state, sprite); + + if (fileWanted) { + try { + writeSidecar(sidecarPath, frame); + } catch (e) { + logger.debug("beacon buddy: sidecar write failed", {}, e as Error); + } + } + renderer?.draw(frame); + + prevFork = fork; + }; + + node.chain.clock.on(ClockEvent.slot, onSlot); + node.chain.emitter.on(routes.events.EventType.chainReorg, onReorg); + + return { + stop: () => { + try { + node.chain.clock.off(ClockEvent.slot, onSlot); + node.chain.emitter.off(routes.events.EventType.chainReorg, onReorg); + } catch { + // ignore + } + renderer?.stop(); + }, + }; +} + +function safeCall(fn: () => T, fallback: T): T { + try { + return fn(); + } catch { + return fallback; + } +} diff --git a/packages/cli/src/util/buddy/render.ts b/packages/cli/src/util/buddy/render.ts new file mode 100644 index 000000000000..62672140fd0f --- /dev/null +++ b/packages/cli/src/util/buddy/render.ts @@ -0,0 +1,117 @@ +import {writeFile600Perm} from "../file.js"; +import {SPRITE_HEIGHT} from "./sprites.js"; +import {BuddyState} from "./types.js"; + +const MIN_ROWS = 10; +const FRAME_LINES = SPRITE_HEIGHT + 2; + +export function formatHeader(state: BuddyState): string { + return `[slot ${state.slot} | epoch ${state.epoch} | ${state.fork} | peers ${state.peers} | mood: ${state.mood}]`; +} + +export function formatFooter(state: BuddyState): string { + const sync = state.synced ? "synced [ok]" : `syncing (${state.syncDistance} behind)`; + return sync; +} + +export function formatFrame(state: BuddyState, sprite: string[]): string { + return [formatHeader(state), ...sprite, formatFooter(state)].join("\n") + "\n"; +} + +export function writeSidecar(filepath: string, frame: string): void { + writeFile600Perm(filepath, frame); +} + +/** + * Pins a fixed-height frame to the bottom of the terminal by setting a DECSTBM + * scroll region for everything above. Concurrent logger writes scroll inside + * the top region; the pinned frame is redrawn on demand. + */ +export class TtyRenderer { + private readonly stream: NodeJS.WriteStream; + private readonly height: number; + private started = false; + private lastFrame: string | null = null; + private resizeListener: (() => void) | null = null; + + constructor(stream: NodeJS.WriteStream = process.stdout, height: number = FRAME_LINES) { + this.stream = stream; + this.height = height; + } + + static isSupported(stream: NodeJS.WriteStream = process.stdout): boolean { + return Boolean(stream.isTTY) && (stream.rows ?? 0) >= MIN_ROWS; + } + + private rows(): number { + return this.stream.rows ?? 24; + } + + private setScrollRegion(): void { + const rows = this.rows(); + const top = 1; + const bottom = Math.max(top, rows - this.height); + // DECOM off: row addressing stays absolute, not relative to scroll region. + this.stream.write("\x1b[?6l"); + // DECSTBM: set scroll region [top;bottom]. Logger output stays above. + this.stream.write(`\x1b[${top};${bottom}r`); + // Park cursor at bottom of scroll region so subsequent log lines flow there. + this.stream.write(`\x1b[${bottom};1H`); + } + + private drawPinned(frame: string): void { + const rows = this.rows(); + const pinnedTop = Math.max(1, rows - this.height + 1); + // Save cursor (DECSC), move to pinned area, clear, write, restore (DECRC). + this.stream.write("\x1b7"); + this.stream.write(`\x1b[${pinnedTop};1H`); + this.stream.write("\x1b[0J"); + this.stream.write(frame); + this.stream.write("\x1b8"); + } + + /** Returns true if the terminal is currently too small to host the pinned frame. */ + private tooSmall(): boolean { + return this.rows() < MIN_ROWS; + } + + draw(frame: string): void { + this.lastFrame = frame; + if (this.tooSmall()) { + // Terminal shrank below minimum after start: tear down pinned mode so the + // user gets their full screen back. File mode keeps the buddy alive. + if (this.started) this.stop(); + return; + } + if (!this.started) { + // Reserve room: scroll up by height so existing content shifts above. + this.stream.write("\n".repeat(this.height)); + this.setScrollRegion(); + this.resizeListener = () => { + if (this.tooSmall()) { + if (this.started) this.stop(); + return; + } + this.setScrollRegion(); + if (this.lastFrame) this.drawPinned(this.lastFrame); + }; + this.stream.on("resize", this.resizeListener); + this.started = true; + } + this.drawPinned(frame); + } + + stop(): void { + if (!this.started) return; + if (this.resizeListener) { + this.stream.off("resize", this.resizeListener); + this.resizeListener = null; + } + // Reset scroll region to full screen, move cursor to bottom, newline so + // the shell prompt lands cleanly below any final logs. + this.stream.write("\x1b[r"); + this.stream.write(`\x1b[${this.rows()};1H\n`); + this.started = false; + this.lastFrame = null; + } +} diff --git a/packages/cli/src/util/buddy/sprites.ts b/packages/cli/src/util/buddy/sprites.ts new file mode 100644 index 000000000000..92deaeeb32fb --- /dev/null +++ b/packages/cli/src/util/buddy/sprites.ts @@ -0,0 +1,32 @@ +import {Mood, SpriteKind} from "./types.js"; + +export const SPRITE_HEIGHT = 4; + +type Face = {eyes: string; mouth: string}; + +const FACES: Record = { + [Mood.happy]: {eyes: "^ ^", mouth: "\\_/"}, + [Mood.sleepy]: {eyes: "- -", mouth: "___"}, + [Mood.panic]: {eyes: "O O", mouth: "/~\\"}, + [Mood.sad]: {eyes: "; ;", mouth: "/-\\"}, +}; + +function renderMood(mood: Mood): string[] { + const {eyes, mouth} = FACES[mood]; + return [" .---. ", ` / ${eyes} \\ `, ` \\ ${mouth} / `, " '---' "]; +} + +const STARBURST: string[] = [" \\ | / ", " --*--*-- ", " / | \\ ", " "]; + +function renderRare(label?: string): string[] { + const base = STARBURST.slice(); + if (label) { + base[3] = ` ${label.slice(0, 8).padEnd(8, " ")}`; + } + return base; +} + +export function renderSprite(kind: SpriteKind): string[] { + if (kind.kind === "mood") return renderMood(kind.mood); + return renderRare(kind.label); +} diff --git a/packages/cli/src/util/buddy/types.ts b/packages/cli/src/util/buddy/types.ts new file mode 100644 index 000000000000..0201932285c4 --- /dev/null +++ b/packages/cli/src/util/buddy/types.ts @@ -0,0 +1,28 @@ +export enum Mood { + happy = "happy", + sleepy = "sleepy", + panic = "panic", + sad = "sad", +} + +export enum RareSprite { + slot1337 = "slot1337", + slot31337 = "slot31337", + slotMillion = "slotMillion", + forkActivation = "forkActivation", +} + +export type SpriteKind = {kind: "mood"; mood: Mood} | {kind: "rare"; rare: RareSprite; label?: string}; + +export type BuddyMode = "off" | "file" | "tty" | "both"; + +export type BuddyState = { + slot: number; + epoch: number; + fork: string; + peers: number; + synced: boolean; + syncDistance: number; + mood: Mood; + override?: {rare: RareSprite; label?: string}; +}; diff --git a/packages/cli/test/unit/util/buddy.test.ts b/packages/cli/test/unit/util/buddy.test.ts new file mode 100644 index 000000000000..87431c19121d --- /dev/null +++ b/packages/cli/test/unit/util/buddy.test.ts @@ -0,0 +1,88 @@ +import {describe, expect, it} from "vitest"; +import {Telemetry, buildState, computeMood, computeRare, spriteKindFor} from "../../../src/util/buddy/buddy.js"; +import {formatFrame} from "../../../src/util/buddy/render.js"; +import {renderSprite} from "../../../src/util/buddy/sprites.js"; +import {Mood, RareSprite} from "../../../src/util/buddy/types.js"; + +function telemetry(overrides: Partial = {}): Telemetry { + return { + slot: 100, + peers: 30, + isSyncing: false, + syncDistance: 0, + fork: "deneb", + prevFork: "deneb", + lastReorgSlot: null, + ...overrides, + }; +} + +describe("buddy / computeMood", () => { + it("panic when peers === 0", () => { + expect(computeMood(telemetry({peers: 0}))).toBe(Mood.panic); + }); + it("sad when reorg within lookback", () => { + expect(computeMood(telemetry({slot: 100, lastReorgSlot: 80}))).toBe(Mood.sad); + }); + it("sleepy when syncing and far behind", () => { + expect(computeMood(telemetry({isSyncing: true, syncDistance: 1000}))).toBe(Mood.sleepy); + }); + it("happy default", () => { + expect(computeMood(telemetry())).toBe(Mood.happy); + }); + it("panic takes priority over sad", () => { + expect(computeMood(telemetry({peers: 0, lastReorgSlot: 99}))).toBe(Mood.panic); + }); + it("sad takes priority over sleepy", () => { + expect(computeMood(telemetry({lastReorgSlot: 80, isSyncing: true, syncDistance: 1000}))).toBe(Mood.sad); + }); + it("ignores stale reorg outside lookback", () => { + expect(computeMood(telemetry({slot: 200, lastReorgSlot: 100}))).toBe(Mood.happy); + }); +}); + +describe("buddy / computeRare", () => { + it("triggers on slot 1337", () => { + expect(computeRare(telemetry({slot: 1337}))?.rare).toBe(RareSprite.slot1337); + }); + it("triggers on slot 31337", () => { + expect(computeRare(telemetry({slot: 31337}))?.rare).toBe(RareSprite.slot31337); + }); + it("triggers on millionth slot", () => { + expect(computeRare(telemetry({slot: 1_000_000}))?.rare).toBe(RareSprite.slotMillion); + }); + it("triggers on 2 millionth slot with 2M label", () => { + const r = computeRare(telemetry({slot: 2_000_000})); + expect(r?.rare).toBe(RareSprite.slotMillion); + expect(r?.label).toBe("2M"); + }); + it("triggers on fork activation slot", () => { + expect(computeRare(telemetry({fork: "electra", prevFork: "deneb"}))?.rare).toBe(RareSprite.forkActivation); + }); + it("returns null on ordinary slot", () => { + expect(computeRare(telemetry({slot: 12345}))).toBeNull(); + }); +}); + +describe("buddy / buildState + formatFrame", () => { + it("formats sidecar frame with header, sprite, footer", () => { + const state = buildState(telemetry({slot: 8123456, peers: 47, fork: "gloas"})); + const sprite = renderSprite(spriteKindFor(state)); + const frame = formatFrame(state, sprite); + expect(frame).toContain("[slot 8123456"); + expect(frame).toContain("peers 47"); + expect(frame).toContain("mood: happy"); + expect(frame).toContain("synced [ok]"); + expect(frame.split("\n").length).toBeGreaterThan(4); + }); + it("shows syncing footer when not synced", () => { + const state = buildState(telemetry({isSyncing: true, syncDistance: 100})); + const frame = formatFrame(state, renderSprite(spriteKindFor(state))); + expect(frame).toContain("syncing (100 behind)"); + }); + it("uses rare sprite when override present", () => { + const state = buildState(telemetry({slot: 1337})); + expect(state.override?.rare).toBe(RareSprite.slot1337); + expect(spriteKindFor(state).kind).toBe("rare"); + }); +}); From 4337d65676863a289166a8d73f981ba1d6b70445 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 26 May 2026 18:59:10 -0700 Subject: [PATCH 2/4] fix(cli): restore terminal on abort path and require --fun value Call buddy.stop() in the abort signal listener too, so an error or programmatic abort that bypasses onGracefulShutdown still resets the DECSTBM scroll region. stop() is idempotent. Drop the unreliable bare-flag coerce on --fun: yargs swallows the next token (`--fun --reset` made fun="--reset"), so require an explicit value. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/cli/src/cmds/beacon/handler.ts | 1 + packages/cli/src/cmds/beacon/options.ts | 9 +++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/cmds/beacon/handler.ts b/packages/cli/src/cmds/beacon/handler.ts index 40ab8e58c76e..d36c4c0ce68c 100644 --- a/packages/cli/src/cmds/beacon/handler.ts +++ b/packages/cli/src/cmds/beacon/handler.ts @@ -151,6 +151,7 @@ export async function beaconHandler(args: BeaconArgs & GlobalArgs): Promise { + buddy?.stop(); try { await node.close(); logger.debug("Beacon node closed"); diff --git a/packages/cli/src/cmds/beacon/options.ts b/packages/cli/src/cmds/beacon/options.ts index e32b503cdbfb..d3494cf0dcbe 100644 --- a/packages/cli/src/cmds/beacon/options.ts +++ b/packages/cli/src/cmds/beacon/options.ts @@ -173,15 +173,12 @@ export const beaconExtraOptions: CliCommandOptions = { fun: { hidden: true, - description: "Enable beacon buddy ASCII pet easter egg (off|tty|file|both)", + // A value is required: yargs would otherwise swallow the next CLI token + // (e.g. `--fun --reset` would set fun="--reset"). Use `--fun both` or `--fun=both`. + description: "Enable beacon buddy ASCII pet easter egg. Requires a value: off|tty|file|both", type: "string", choices: ["off", "tty", "file", "both"], default: "off", - coerce: (v: string | boolean | undefined): "off" | "tty" | "file" | "both" => { - // Bare `--fun` (no value) resolves to "both" - if (v === true || v === "" || v === undefined) return "both"; - return v as "off" | "tty" | "file" | "both"; - }, }, }; From 1d589805a899bbf4f1333830ef153ab5d8891cc9 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 26 May 2026 19:03:36 -0700 Subject: [PATCH 3/4] fix(cli): isolate beacon buddy errors from chain emitter Wrap onSlot and onReorg bodies in try/catch so a synchronous throw in the easter egg can never bubble up through ClockEvent.slot or chainReorg listeners and crash the beacon node. Also safeCall around getForkName and a try/catch around renderer.draw for the same reason. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/cli/src/util/buddy/index.ts | 62 +++++++++++++++++----------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/packages/cli/src/util/buddy/index.ts b/packages/cli/src/util/buddy/index.ts index 74c4acb21dbb..f0a80041be92 100644 --- a/packages/cli/src/util/buddy/index.ts +++ b/packages/cli/src/util/buddy/index.ts @@ -40,40 +40,54 @@ export function startBeaconBuddy({node, dataDir, mode, logger}: StartBuddyOpts): } } - const onReorg = ({slot}: routes.events.EventData[routes.events.EventType.chainReorg]): void => { - lastReorgSlot = slot; + // Easter-egg listeners must never escape an error into the beacon node's + // event emitters: wrap every body so the consensus path is unaffected. + const onReorg = (data: routes.events.EventData[routes.events.EventType.chainReorg]): void => { + try { + lastReorgSlot = data.slot; + } catch (e) { + logger.debug("beacon buddy: onReorg failed", {}, e as Error); + } }; const onSlot = (slot: number): void => { - const peers = safeCall(() => node.network.getConnectedPeerCount(), 0); - const sync = safeCall(() => node.sync.getSyncStatus(), null); - const fork = node.chain.config.getForkName(slot); - if (prevFork === "") prevFork = fork; + try { + const peers = safeCall(() => node.network.getConnectedPeerCount(), 0); + const sync = safeCall(() => node.sync.getSyncStatus(), null); + const fork = safeCall(() => node.chain.config.getForkName(slot), "unknown"); + if (prevFork === "") prevFork = fork; - const telemetry: Telemetry = { - slot, - peers, - isSyncing: sync ? sync.isSyncing : false, - syncDistance: sync ? Number(sync.syncDistance) : 0, - fork, - prevFork, - lastReorgSlot, - }; + const telemetry: Telemetry = { + slot, + peers, + isSyncing: sync ? sync.isSyncing : false, + syncDistance: sync ? Number(sync.syncDistance) : 0, + fork, + prevFork, + lastReorgSlot, + }; - const state = buildState(telemetry); - const sprite = renderSprite(spriteKindFor(state)); - const frame = formatFrame(state, sprite); + const state = buildState(telemetry); + const sprite = renderSprite(spriteKindFor(state)); + const frame = formatFrame(state, sprite); - if (fileWanted) { + if (fileWanted) { + try { + writeSidecar(sidecarPath, frame); + } catch (e) { + logger.debug("beacon buddy: sidecar write failed", {}, e as Error); + } + } try { - writeSidecar(sidecarPath, frame); + renderer?.draw(frame); } catch (e) { - logger.debug("beacon buddy: sidecar write failed", {}, e as Error); + logger.debug("beacon buddy: tty draw failed", {}, e as Error); } - } - renderer?.draw(frame); - prevFork = fork; + prevFork = fork; + } catch (e) { + logger.debug("beacon buddy: onSlot failed", {}, e as Error); + } }; node.chain.clock.on(ClockEvent.slot, onSlot); From 04486579cb51c1ae3ad109da307a7a1b691fb48c Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Tue, 26 May 2026 19:18:47 -0700 Subject: [PATCH 4/4] style(beacon-node): sort util/index.ts exports Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/beacon-node/src/util/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/src/util/index.ts b/packages/beacon-node/src/util/index.ts index 8acff4996c05..852437e3bbfd 100644 --- a/packages/beacon-node/src/util/index.ts +++ b/packages/beacon-node/src/util/index.ts @@ -1,3 +1,4 @@ // Only export the functions which are useful for external packages and difficult to reproduce or mock -export * from "./kzg.js"; + export {ClockEvent} from "./clock.js"; +export * from "./kzg.js";