From ae9a4b7a29dd4f9ac54c1b249ab9883da0197357 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 10 Nov 2026 01:19:28 -0600 Subject: [PATCH] fix(workspaces): keep the final lines of a long older setup log on the task Co-Authored-By: Claude --- server/src/__tests__/task-interruption-query.test.ts | 4 ++++ server/src/services/task-interruption-presentation.ts | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/server/src/__tests__/task-interruption-query.test.ts b/server/src/__tests__/task-interruption-query.test.ts index 59d4bad1f43..89368dceef8 100644 --- a/server/src/__tests__/task-interruption-query.test.ts +++ b/server/src/__tests__/task-interruption-query.test.ts @@ -384,6 +384,10 @@ describe.skipIf(!support.supported)("task interruption latest run query", () => const text = JSON.stringify(presentation); for (let index = 0; index + 8 <= secret.length; index += 1) expect(text).not.toContain(secret.slice(index, index + 8)); } + // A long older log still shows its final error line. + await db.update(heartbeatRuns).set({ error: `Execution workspace provision command "${command}" failed: ${"filler\n".repeat(800)}No such file or directory` }) + .where(eq(heartbeatRuns.id, run!.id)); + for (const presentation of await read()) expect(presentation?.setupFailure?.output).toMatch(/No such file or directory$/); // A transient retry shows its setup_failed predecessor's cause. const predecessorId = randomUUID(); await db.insert(heartbeatRuns).values({ id: predecessorId, companyId: f.companyId, agentId: run!.agentId, status: "failed", diff --git a/server/src/services/task-interruption-presentation.ts b/server/src/services/task-interruption-presentation.ts index 2cbd5f2b52b..5009f22e5f6 100644 --- a/server/src/services/task-interruption-presentation.ts +++ b/server/src/services/task-interruption-presentation.ts @@ -629,9 +629,11 @@ export async function taskInterruptionPresentations(db: Db, companyId: string, i } if (presentation.setupFailure) { const value = safe[index]?.setupFailure; - const command = safeMessage(value?.command), message = safeMessage(value?.message); + // Redact the whole text, then shorten: the output keeps its final lines, not a head slice. + const whole = (text: unknown) => typeof text === "string" ? redactCurrentUserText(text, { enabled: settings.censorUsernameInLogs }) : ""; + const command = whole(value?.command), message = whole(value?.message); const failure = value && command.trim() && message.trim() - ? boundSetupCommandFailure({ ...value, command, output: value.output == null ? null : safeMessage(value.output) || null, message }) + ? boundSetupCommandFailure({ ...value, command, output: value.output == null ? null : whole(value.output) || null, message }) : null; if (failure) presentation.setupFailure = failure; else delete presentation.setupFailure;