Fix test failure and address Greptile review comments

- Fix "keeps alive" test: set lastOutputAt to current time so idle
  reaper doesn't kill the test run (seed used a stale date)
- Wire up lastOutputAt and startedAt params in test seed fixture
- Remove dead wasIdle variable in clearDetachedRunWarning
- Fix deferred lastOutputAt flush to use actual last output timestamp
  instead of wall-clock time at flush

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin Foley 2026-03-26 23:33:41 -07:00
parent 942d023148
commit aa88db7238
2 changed files with 11 additions and 7 deletions

View file

@ -129,7 +129,8 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
processLossRetryCount: input?.processLossRetryCount ?? 0,
errorCode: input?.runErrorCode ?? null,
error: input?.runError ?? null,
startedAt: now,
startedAt: input?.startedAt ?? now,
lastOutputAt: input?.lastOutputAt ?? null,
updatedAt: new Date("2026-03-19T00:00:00.000Z"),
});
@ -159,6 +160,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
const { runId, wakeupRequestId } = await seedRunFixture({
processPid: child.pid ?? null,
includeIssue: false,
lastOutputAt: new Date(),
});
const heartbeat = heartbeatService(db);

View file

@ -1506,7 +1506,6 @@ export function heartbeatService(db: Db) {
.then((rows) => rows[0] ?? null);
if (!updated) return null;
const wasIdle = updated.errorCode === null; // errorCode was cleared
await appendRunEvent(updated, await nextRunEventSeq(updated.id), {
eventType: "lifecycle",
stream: "system",
@ -2509,27 +2508,30 @@ export function heartbeatService(db: Db) {
const currentUserRedactionOptions = await getCurrentUserRedactionOptions();
let lastOutputAtFlushPending = false;
let lastOutputAtLatest: Date | null = null;
const onLog = async (stream: "stdout" | "stderr", chunk: string) => {
const sanitizedChunk = redactCurrentUserText(chunk, currentUserRedactionOptions);
if (stream === "stdout") stdoutExcerpt = appendExcerpt(stdoutExcerpt, sanitizedChunk);
if (stream === "stderr") stderrExcerpt = appendExcerpt(stderrExcerpt, sanitizedChunk);
const ts = new Date().toISOString();
lastOutputAtLatest = new Date(ts);
// Batch lastOutputAt writes — flush at most once per 30 seconds to avoid DB churn
if (!lastOutputAtFlushPending) {
lastOutputAtFlushPending = true;
// Flush immediately on first output
await db.update(heartbeatRuns)
.set({ lastOutputAt: lastOutputAtLatest, updatedAt: new Date() })
.where(eq(heartbeatRuns.id, runId));
setTimeout(() => {
lastOutputAtFlushPending = false;
if (!lastOutputAtLatest) return;
db.update(heartbeatRuns)
.set({ lastOutputAt: new Date(), updatedAt: new Date() })
.set({ lastOutputAt: lastOutputAtLatest, updatedAt: new Date() })
.where(eq(heartbeatRuns.id, runId))
.then(() => {})
.catch((err) => logger.warn({ err, runId }, "failed to flush lastOutputAt"));
}, 30_000);
// Also flush immediately on first output
await db.update(heartbeatRuns)
.set({ lastOutputAt: new Date(ts), updatedAt: new Date() })
.where(eq(heartbeatRuns.id, runId));
}
if (handle) {