From 85e6371cb6dfd01ca703262975b3216d631fa693 Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 2 Apr 2026 09:18:26 -0500 Subject: [PATCH] fix: use agent role for first heartbeat telemetry Co-Authored-By: Paperclip --- .../heartbeat-process-recovery.test.ts | 37 ++++++++++++++++++- server/src/services/heartbeat.ts | 2 +- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/server/src/__tests__/heartbeat-process-recovery.test.ts b/server/src/__tests__/heartbeat-process-recovery.test.ts index 6b18d162..77538331 100644 --- a/server/src/__tests__/heartbeat-process-recovery.test.ts +++ b/server/src/__tests__/heartbeat-process-recovery.test.ts @@ -1,7 +1,7 @@ import { randomUUID } from "node:crypto"; import { spawn, type ChildProcess } from "node:child_process"; import { eq } from "drizzle-orm"; -import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { agents, agentWakeupRequests, @@ -16,6 +16,23 @@ import { startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; import { runningProcesses } from "../adapters/index.ts"; +const mockTelemetryClient = vi.hoisted(() => ({ track: vi.fn() })); +const mockTrackAgentFirstHeartbeat = vi.hoisted(() => vi.fn()); + +vi.mock("../telemetry.ts", () => ({ + getTelemetryClient: () => mockTelemetryClient, +})); + +vi.mock("@paperclipai/shared/telemetry", async () => { + const actual = await vi.importActual( + "@paperclipai/shared/telemetry", + ); + return { + ...actual, + trackAgentFirstHeartbeat: mockTrackAgentFirstHeartbeat, + }; +}); + import { heartbeatService } from "../services/heartbeat.ts"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; @@ -43,6 +60,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { }, 20_000); afterEach(async () => { + vi.clearAllMocks(); runningProcesses.clear(); for (const child of childProcesses) { child.kill("SIGKILL"); @@ -67,6 +85,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { async function seedRunFixture(input?: { adapterType?: string; + agentStatus?: "paused" | "idle" | "running"; runStatus?: "running" | "queued" | "failed"; processPid?: number | null; processLossRetryCount?: number; @@ -94,7 +113,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { companyId, name: "CodexCoder", role: "engineer", - status: "paused", + status: input?.agentStatus ?? "paused", adapterType: input?.adapterType ?? "codex_local", adapterConfig: {}, runtimeConfig: {}, @@ -252,4 +271,18 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { expect(run?.errorCode).toBeNull(); expect(run?.error).toBeNull(); }); + + it("tracks the first heartbeat with the agent role instead of adapter type", async () => { + const { runId } = await seedRunFixture({ + agentStatus: "running", + includeIssue: false, + }); + const heartbeat = heartbeatService(db); + + await heartbeat.cancelRun(runId); + + expect(mockTrackAgentFirstHeartbeat).toHaveBeenCalledWith(mockTelemetryClient, { + agentRole: "engineer", + }); + }); }); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 507bf663..356783de 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -1832,7 +1832,7 @@ export function heartbeatService(db: Db) { if (isFirstHeartbeat && updated) { const tc = getTelemetryClient(); - if (tc) trackAgentFirstHeartbeat(tc, { agentRole: updated.adapterType }); + if (tc) trackAgentFirstHeartbeat(tc, { agentRole: updated.role }); } if (updated) {