mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-03 17:51:58 +01:00
fix: bound runtime status cleanup to active writes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
76fde891cc
commit
03ef5469a1
2 files changed
+54
-1
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearAllHeartbeatRunRuntimeStatuses,
|
||||
clearHeartbeatRunRuntimeStatus,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
describe("heartbeat run runtime status store", () => {
|
||||
afterEach(() => {
|
||||
clearAllHeartbeatRunRuntimeStatuses();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("stores scoped ephemeral status and expires stale entries", () => {
|
||||
@@ -166,6 +167,43 @@ describe("heartbeat run runtime status store", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("cleans unread abandoned statuses on later output without polling or expiring fresh context", () => {
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
const start = new Date("2026-10-30T00:00:00Z");
|
||||
vi.setSystemTime(start);
|
||||
const write = (runId: string) => setHeartbeatRunRuntimeStatus({
|
||||
companyId: "company-1", issueId: null, agentId: "agent-1", runId,
|
||||
phase: "run_activity", message: "Using Bash",
|
||||
});
|
||||
write("abandoned");
|
||||
write("active");
|
||||
vi.setSystemTime(start.getTime() + 60_000);
|
||||
touchHeartbeatRunRuntimeStatus({
|
||||
companyId: "company-1", issueId: null, agentId: "agent-1", runId: "active",
|
||||
});
|
||||
vi.setSystemTime(start.getTime() + 90_000);
|
||||
write("boundary");
|
||||
expect(getHeartbeatRunRuntimeStatus("abandoned")).toMatchObject({ message: "Using Bash" });
|
||||
expect(clearHeartbeatRunRuntimeStatus("boundary")).toBe(true);
|
||||
vi.setSystemTime(start.getTime() + 90_001);
|
||||
touchHeartbeatRunRuntimeStatus({
|
||||
companyId: "company-1", issueId: null, agentId: "agent-1", runId: "active",
|
||||
});
|
||||
expect(clearHeartbeatRunRuntimeStatus("abandoned")).toBe(false);
|
||||
expect(getHeartbeatRunRuntimeStatus("active")).toMatchObject({ message: "Using Bash" });
|
||||
setHeartbeatRunRuntimeStatus({
|
||||
companyId: "company-1", issueId: null, agentId: "agent-1", runId: "old-output",
|
||||
phase: "run_activity", message: "Delayed output", updatedAt: start,
|
||||
});
|
||||
vi.setSystemTime(start.getTime() + 90_002);
|
||||
write("next-output");
|
||||
expect(clearHeartbeatRunRuntimeStatus("old-output")).toBe(true);
|
||||
vi.setSystemTime(start.getTime() + 180_002);
|
||||
write("new");
|
||||
expect(clearHeartbeatRunRuntimeStatus("active")).toBe(false);
|
||||
expect(getHeartbeatRunRuntimeStatus("new")).toMatchObject({ message: "Using Bash" });
|
||||
});
|
||||
|
||||
it("sweeps expired statuses without touching fresh entries", () => {
|
||||
setHeartbeatRunRuntimeStatus({
|
||||
companyId: "company-1",
|
||||
|
||||
@@ -20,6 +20,18 @@ export interface HeartbeatRunRuntimeStatus {
|
||||
}
|
||||
|
||||
const runtimeStatusesByRunId = new Map<string, HeartbeatRunRuntimeStatus>();
|
||||
let nextWriteCleanupAt: number | null = null;
|
||||
|
||||
function cleanupExpiredStatusesOnWrite() {
|
||||
const now = Date.now();
|
||||
if (nextWriteCleanupAt === null) {
|
||||
nextWriteCleanupAt = now + HEARTBEAT_RUN_RUNTIME_STATUS_TTL_MS;
|
||||
return;
|
||||
}
|
||||
if (now <= nextWriteCleanupAt) return;
|
||||
sweepExpiredHeartbeatRunRuntimeStatuses(new Date(now));
|
||||
nextWriteCleanupAt = now + HEARTBEAT_RUN_RUNTIME_STATUS_TTL_MS;
|
||||
}
|
||||
|
||||
function cloneStatus(status: HeartbeatRunRuntimeStatus): HeartbeatRunRuntimeStatus {
|
||||
return {
|
||||
@@ -64,6 +76,7 @@ export function setHeartbeatRunRuntimeStatus(
|
||||
lastEventAt?: Date | null;
|
||||
},
|
||||
): HeartbeatRunRuntimeStatus | null {
|
||||
cleanupExpiredStatusesOnWrite();
|
||||
const message = sanitizeHeartbeatRunRuntimeStatusMessage(input.message);
|
||||
if (!message) {
|
||||
clearHeartbeatRunRuntimeStatus(input.runId);
|
||||
@@ -106,6 +119,7 @@ export function touchHeartbeatRunRuntimeStatus(input: {
|
||||
fallbackPhase?: HeartbeatRunStatusPhase;
|
||||
fallbackMessage?: string;
|
||||
}): HeartbeatRunRuntimeStatus | null {
|
||||
cleanupExpiredStatusesOnWrite();
|
||||
const at = input.at ?? new Date();
|
||||
const existing = runtimeStatusesByRunId.get(input.runId);
|
||||
if (
|
||||
@@ -167,6 +181,7 @@ export function clearHeartbeatRunRuntimeStatus(runId: string): boolean {
|
||||
|
||||
export function clearAllHeartbeatRunRuntimeStatuses(): void {
|
||||
runtimeStatusesByRunId.clear();
|
||||
nextWriteCleanupAt = null;
|
||||
}
|
||||
|
||||
export function sweepExpiredHeartbeatRunRuntimeStatuses(
|
||||
|
||||
Reference in new issue
Block a user