();
@@ -1477,6 +1491,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
if (liveRun && source.id === liveRun.id) continue;
const entries = transcriptByRun.get(source.id) ?? [];
const meta = linkedRunMetaById.get(source.id);
+ const historical = statusRelevance.isHistoricalRun(source.id);
// A workspace admission attempt never started provider work. Its live
// successor owns the waiting indicator; retain this attempt in the run log.
if (source.status === "cancelled" && meta?.errorCode === "workspace_busy") {
@@ -1508,11 +1523,12 @@ export function TaskChatThread(props: TaskChatThreadProps) {
entries.length === 0 &&
!lastCommentIdByRun.has(source.id);
if (executionWait) {
+ settledRunIds.add(source.id);
+ if (historical) continue;
const wait = meta?.resultJson?.executionWait;
const waitKey = wait && typeof wait === "object" && "recoveryActionId" in wait
? String(wait.recoveryActionId)
: "execution_reconciliation_required";
- settledRunIds.add(source.id);
if (previousExecutionWaitKey !== waitKey) {
const id = `${source.id}:execution-wait`;
entriesWithFailures.push({
@@ -1557,7 +1573,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
const progressCommentIds = semanticProgressCommentIds(meta?.resultJson);
const sourcePresentationCommentId =
decidedCommentId === undefined
- ? ([...comments]
+ ? ([...projectedComments]
.reverse()
.find(
(comment) =>
@@ -1566,7 +1582,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
!progressCommentIds.has(comment.id),
)?.id ?? null)
: decidedCommentId !== null &&
- comments.some(
+ projectedComments.some(
(comment) =>
!comment.deletedAt &&
comment.runId === source.id &&
@@ -1576,7 +1592,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
: null;
const sourceHasPresentationComment = sourcePresentationCommentId !== null;
const sourcePresentationText = sourcePresentationCommentId
- ? (comments.find(
+ ? (projectedComments.find(
(comment) => comment.id === sourcePresentationCommentId,
)?.body ?? null)
: null;
@@ -1643,7 +1659,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
: undefined;
const finishedAt =
meta?.finishedAt ?? meta?.startedAt ?? meta?.createdAt;
- entriesWithFailures.push({
+ if (!historical) entriesWithFailures.push({
ms: toMs(finishedAt),
order: 3,
id,
@@ -1701,7 +1717,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
? `Workspace setup failed before the agent started. ${retryDetail}`
: `The run failed (${code}). ${retryDetail}`;
const id = `${source.id}:failure`;
- entriesWithFailures.push({
+ if (!historical) entriesWithFailures.push({
ms: toMs(meta?.finishedAt ?? meta?.startedAt ?? meta?.createdAt),
order: 3,
id,
@@ -1750,6 +1766,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
id: turnId,
kind: "turn",
settled: true,
+ historical,
agentName:
meta?.agentName ??
(meta?.agentId ? agentMap?.get(meta.agentId)?.name : undefined),
@@ -1775,7 +1792,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
} else if (!sourceHasNativeStop && !sourceHasLegacyStop && !lastCommentIdByRun.has(source.id)) {
settledRunIds.add(source.id);
const id = `${source.id}:terminal-notice`;
- entriesWithFailures.push({
+ if (!historical) entriesWithFailures.push({
ms: toMs(meta?.finishedAt ?? meta?.startedAt ?? meta?.createdAt),
order: 3,
id,
@@ -1807,7 +1824,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
!sourceHasNativeResponse
) {
const id = `${source.id}:terminal-notice`;
- entriesWithFailures.push({
+ if (!historical) entriesWithFailures.push({
ms: toMs(meta?.finishedAt ?? meta?.startedAt ?? meta?.createdAt),
order: 3,
id,
@@ -1843,12 +1860,13 @@ export function TaskChatThread(props: TaskChatThreadProps) {
source.id === planDocumentSourceRunId && planTurnItem
? embedPlanDocumentAtWriteBoundary(parsedTranscript, planTurnItem)
: parsedTranscript;
+ const visibleActivity = historical ? withoutHistoricalRunStatus(parsed) : parsed;
return {
segment,
parsed,
timelineItems: sourceIsPaperclipRunner
- ? paperclipRunnerTimelineItems(parsed)
- : parsed,
+ ? paperclipRunnerTimelineItems(visibleActivity)
+ : visibleActivity,
};
});
const sourceResponseText =
@@ -1928,6 +1946,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
id: turnId,
kind: "turn",
settled: true,
+ historical,
agentName:
meta?.agentName ??
(meta?.agentId ? agentMap?.get(meta.agentId)?.name : undefined),
@@ -2090,6 +2109,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
};
}, [
orderedEntries,
+ statusRelevance,
canRetryFailedRun,
interactions,
runs,
@@ -2098,6 +2118,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
linkedRunMetaById,
lastCommentIdByRun,
comments,
+ projectedComments,
steeringAnchorsByRun,
legacyTimelineAnchorsByRun,
hasBrief,
diff --git a/ui/src/components/task-chat/TaskChatTurn.test.tsx b/ui/src/components/task-chat/TaskChatTurn.test.tsx
index 5b256850fb0..bfdc95d4d0d 100644
--- a/ui/src/components/task-chat/TaskChatTurn.test.tsx
+++ b/ui/src/components/task-chat/TaskChatTurn.test.tsx
@@ -171,6 +171,27 @@ describe("TaskChatTurn", () => {
'[data-testid="task-chat-live-turn"]',
);
+ it("keeps historical activity expandable without a failure or completion status", () => {
+ renderTurn({ ...SETTLED, historical: true, summary: { ...SETTLED.summary, failed: true } });
+ expect(summaryBtn()?.textContent).toContain("Activity");
+ expect(container.textContent).not.toContain("Stopped");
+ expect(container.textContent).not.toContain("Worked");
+ flushSync(() => summaryBtn()!.click());
+ expect(container.textContent).toContain("c1");
+ expect(fold()?.getAttribute("data-folded")).toBe("false");
+ });
+
+ it("omits historical status while preserving native agent identity and response", () => {
+ renderTurn({ ...SETTLED, historical: true, standaloneHeader: true, agentName: "Assistant",
+ summary: { ...SETTLED.summary, failed: true },
+ finalResponse: { id: "response", kind: "message", author: "agent", text: "Saved response" } });
+ expect(container.textContent).toContain("Assistant");
+ expect(container.textContent).toContain("Saved response");
+ expect(container.textContent).toContain("c1");
+ expect(container.textContent).not.toContain("Stopped");
+ expect(container.textContent).not.toContain("Worked");
+ });
+
it("renders a settled turn folded with its summary line", () => {
renderTurn(SETTLED);
// Label and mono metrics are adjacent spans (v7 runsum grammar).
diff --git a/ui/src/components/task-chat/TaskChatTurn.tsx b/ui/src/components/task-chat/TaskChatTurn.tsx
index 3377ef40ed6..71c081cf2a6 100644
--- a/ui/src/components/task-chat/TaskChatTurn.tsx
+++ b/ui/src/components/task-chat/TaskChatTurn.tsx
@@ -88,7 +88,7 @@ export function TaskChatTurn({
data-testid="task-chat-turn"
data-settled={item.settled ? "true" : "false"}
>
-
) : null}
-
+ {!item.historical ?
{item.continuedAfterSteering ? "Continued after steering ยท " : ""}
{item.summary.durationLabel
? `${item.summary.failed ? "Stopped" : "Worked"} for ${item.summary.durationLabel}`
: item.summary.failed
? "Stopped"
: "Worked"}
-
-
+ : null}
+ : null}
{item.items.length > 0 ? (
setOpen((o) => !o)}
@@ -201,11 +202,11 @@ export function TaskChatTurn({
>
) : null}
- {!item.standaloneHeader ? (
+ {!item.standaloneHeader && !item.historical ? (
) : null}
- {item.standaloneHeader && item.summary.durationLabel
+ {item.historical ? "Activity" : item.standaloneHeader && item.summary.durationLabel
? `${item.summary.failed ? "Stopped" : "Worked"} for ${item.summary.durationLabel}`
: item.summary.failed
? "Stopped"
diff --git a/ui/src/components/task-chat/status-relevance.test.ts b/ui/src/components/task-chat/status-relevance.test.ts
new file mode 100644
index 00000000000..a80f31ccf84
--- /dev/null
+++ b/ui/src/components/task-chat/status-relevance.test.ts
@@ -0,0 +1,132 @@
+import { describe, expect, it } from "vitest";
+import type { ExecutionProjection } from "@paperclipai/shared";
+import type { IssueChatComment } from "@/lib/issue-chat-messages";
+import { taskChatStatusRelevance, withoutHistoricalRunStatus } from "./status-relevance";
+import type { TaskChatItem } from "./task-chat-model";
+
+const oldRun = {
+ id: "old", agentId: "agent", status: "failed",
+ createdAt: "2025-01-01T10:00:00Z", startedAt: "2025-01-01T10:00:01Z",
+};
+const nextRun = { ...oldRun, id: "next", status: "succeeded", createdAt: "2025-01-01T10:01:00Z", startedAt: "2025-01-01T10:01:01Z" };
+const hold: ExecutionProjection = {
+ phase: "recovery_needed", label: "Stopped", cause: "uncertain_provider_action",
+ lastConfirmedActivityAt: null, retryAt: null, attempt: 1, maxAttempts: 3,
+ recoveryOwner: "board", nextAction: "Check the execution", permittedActions: ["inspect_recovery"],
+ predecessorRunId: null, successorRunId: null,
+};
+const notice: IssueChatComment = {
+ id: "notice", companyId: "company", issueId: "issue", authorType: "system",
+ authorAgentId: null, authorUserId: null, body: "Execution needs attention.",
+ presentation: null, metadata: { version: 1, sourceRunId: "old", sections: [] },
+ createdAt: new Date("2025-01-01T10:00:30Z"), updatedAt: new Date("2025-01-01T10:00:30Z"),
+};
+
+describe("task chat status relevance", () => {
+ it("uses attempt creation order regardless of API order or a late finish", () => {
+ for (const runs of [[oldRun, nextRun], [nextRun, oldRun]]) {
+ const policy = taskChatStatusRelevance("in_progress", runs);
+ expect(policy.isHistoricalRun("old")).toBe(true);
+ expect(policy.isHistoricalRun("next")).toBe(false);
+ }
+ });
+
+ it("keeps an unresolved failure regardless of its age or missing provenance", () => {
+ const policy = taskChatStatusRelevance("blocked", [oldRun]);
+ expect(policy.isHistoricalRun("old")).toBe(false);
+ expect(policy.isHistoricalRun("unloaded")).toBe(false);
+ expect(policy.isHistoricalNotice(notice)).toBe(false);
+ });
+
+ it("does not treat another agent or a refused admission as a replacement", () => {
+ for (const later of [
+ { ...nextRun, agentId: "other-agent" },
+ { ...nextRun, status: "cancelled", startedAt: null, errorCode: "execution_reconciliation_required" },
+ ]) {
+ expect(taskChatStatusRelevance("blocked", [oldRun, later]).isHistoricalRun("old")).toBe(false);
+ }
+ });
+
+ it("hides earlier observations of a wait and clears the latest wait after work starts", () => {
+ const wait = { ...oldRun, status: "cancelled", startedAt: null, errorCode: "execution_reconciliation_required" };
+ const newerWait = { ...wait, id: "newer-wait", createdAt: "2025-01-01T10:00:30Z" };
+ expect(taskChatStatusRelevance("blocked", [wait, newerWait]).isHistoricalRun("old")).toBe(true);
+ expect(taskChatStatusRelevance("blocked", [wait, newerWait]).isHistoricalRun("newer-wait")).toBe(false);
+ expect(taskChatStatusRelevance("in_progress", [wait, newerWait, nextRun]).isHistoricalRun("newer-wait")).toBe(true);
+ });
+
+ it("preserves an explicit hold until a successor or terminal task resolves it", () => {
+ const held = { ...oldRun, execution: hold };
+ expect(taskChatStatusRelevance("blocked", [held, nextRun]).isHistoricalRun("old")).toBe(false);
+ expect(taskChatStatusRelevance("blocked", [held, oldRun, nextRun]).isHistoricalRun("old")).toBe(false);
+ expect(taskChatStatusRelevance("blocked", [{ ...held, execution: { ...hold, successorRunId: "next" } }]).isHistoricalRun("old")).toBe(true);
+ expect(taskChatStatusRelevance("done", [held]).isHistoricalRun("old")).toBe(true);
+ });
+
+ it("uses resolved recovery evidence even without a successor run", () => {
+ const waiting = { ...oldRun, resultJson: { executionWait: { recoveryActionId: "action" } } };
+ const recoveryNotice = { ...notice, metadata: { version: 1 as const, sections: [], recovery: {
+ kind: "disposition_repair_escalated" as const, actionId: "action", attemptCount: 1,
+ maxAttempts: 3, reason: "missing result", assigneeAgentId: "agent",
+ } } };
+ for (const status of ["active", "escalated", "resolved", "cancelled"] as const) {
+ const action = { id: "action", status };
+ const policy = taskChatStatusRelevance("blocked", [waiting, nextRun], action);
+ expect(policy.isHistoricalRun("old")).toBe(status === "resolved" || status === "cancelled");
+ expect(policy.isHistoricalNotice(recoveryNotice)).toBe(status === "resolved" || status === "cancelled");
+ }
+ });
+
+ it("hides old system notices by provenance without hiding authored messages or session boundaries", () => {
+ const policy = taskChatStatusRelevance("in_progress", [oldRun, nextRun]);
+ expect(policy.isHistoricalNotice({ ...notice, metadata: { version: 1, sourceRunId: "old", sections: [] } })).toBe(true);
+ expect(policy.isHistoricalNotice({ ...notice, metadata: null, createdByRunId: "next" })).toBe(false);
+ expect(policy.isHistoricalNotice({ ...notice, metadata: null, createdByRunId: "unknown" })).toBe(false);
+ expect(policy.isHistoricalNotice(notice)).toBe(true);
+ expect(policy.isHistoricalNotice({ ...notice, authorType: "agent" })).toBe(false);
+ expect(policy.isHistoricalNotice({ ...notice, authorType: "user" })).toBe(false);
+ expect(policy.isHistoricalNotice({ ...notice, conversationSessionGeneration: 1 })).toBe(false);
+ });
+
+ it.each(["done", "cancelled"])("hides obsolete notices on %s tasks even without loaded runs", (status) => {
+ const policy = taskChatStatusRelevance(status, []);
+ expect(policy.isHistoricalRun("unloaded")).toBe(true);
+ expect(policy.isHistoricalNotice(notice)).toBe(true);
+ expect(policy.isHistoricalNotice({ ...notice, authorType: "agent" })).toBe(false);
+ });
+
+ it.each(["in_progress", "done", "cancelled"])("preserves unrelated system updates on %s tasks after later work", (status) => {
+ const policy = taskChatStatusRelevance(status, [oldRun, nextRun]);
+ const relay = { ...notice, metadata: null, body: "A child task is still blocked." };
+ expect(policy.isHistoricalNotice(relay)).toBe(false);
+ expect(policy.isHistoricalNotice({ ...relay, presentation: {
+ kind: "system_notice", title: "Child task blocked", tone: "warning", detailsDefaultOpen: false,
+ } })).toBe(false);
+ });
+
+ it("keeps uncertainty visible when timestamps are missing or tied", () => {
+ const invalid = { ...nextRun, createdAt: "invalid", startedAt: null };
+ expect(taskChatStatusRelevance("blocked", [oldRun, invalid]).isHistoricalRun("old")).toBe(false);
+ expect(taskChatStatusRelevance("blocked", [oldRun, { ...nextRun, createdAt: oldRun.createdAt }]).isHistoricalRun("old")).toBe(false);
+ });
+});
+
+it("removes run lifecycle diagnostics while retaining tool failures and pending permission requests", () => {
+ const retained: TaskChatItem[] = [
+ { id: "reply", kind: "message", author: "agent", text: "Saved the result." },
+ { id: "tool", kind: "tool", name: "Read", status: "failed" },
+ { id: "approval", kind: "protocol", surface: "runtime_request", runId: "old", requestId: "permission",
+ requestKind: "command_approval", turnId: "turn", requestType: "permission", choices: [], fields: [],
+ status: "pending", prompt: "Allow the command?" },
+ ];
+ const items: TaskChatItem[] = [
+ ...retained,
+ { id: "failed", kind: "marker", variant: "interrupted", label: "Run failed" },
+ { id: "system", kind: "message", author: "system", text: "Waiting to resume" },
+ { id: "wait", kind: "protocol", surface: "provider_activity", family: "wait", title: "Waiting", status: "completed",
+ eventType: "wait", details: [], steps: [], links: [], children: [] },
+ { id: "terminal", kind: "protocol", surface: "run_terminal", turnState: "failed", runState: "failed", disposition: "blocked" },
+ ];
+ expect(withoutHistoricalRunStatus(items)).toEqual(retained);
+ expect(items).toHaveLength(7);
+});
diff --git a/ui/src/components/task-chat/status-relevance.ts b/ui/src/components/task-chat/status-relevance.ts
new file mode 100644
index 00000000000..81eafde7580
--- /dev/null
+++ b/ui/src/components/task-chat/status-relevance.ts
@@ -0,0 +1,102 @@
+import type { ExecutionProjection, IssueRecoveryAction } from "@paperclipai/shared";
+import type { IssueChatComment } from "@/lib/issue-chat-messages";
+import { isTerminalIssueStatus } from "@/lib/liveIssueIds";
+import type { TaskChatItem } from "./task-chat-model";
+
+interface StatusRun {
+ id: string;
+ agentId: string;
+ status: string;
+ errorCode?: string | null;
+ createdAt: Date | string;
+ startedAt?: Date | string | null;
+ execution?: ExecutionProjection | null;
+ resultJson?: Record | null;
+}
+
+function timestamp(value: Date | string | null | undefined): number {
+ return value == null ? NaN : new Date(value).getTime();
+}
+
+function isAdmissionWait(run: StatusRun): boolean {
+ return run.status === "cancelled" && !run.startedAt
+ && ["execution_reconciliation_required", "workspace_busy", "issue_not_in_progress", "issue_terminal_status"].includes(run.errorCode ?? "");
+}
+
+/** Presentation only: retain every run and comment in the underlying history. */
+export function taskChatStatusRelevance(
+ issueStatus: string | null | undefined,
+ runs: readonly StatusRun[],
+ recoveryAction?: Pick | null,
+) {
+ const terminal = isTerminalIssueStatus(issueStatus);
+ const byId = new Map();
+ for (const run of runs) {
+ const previous = byId.get(run.id);
+ byId.set(run.id, { ...previous, ...run, execution: run.execution ?? previous?.execution });
+ }
+ const latestByAgent = new Map();
+ const latestAttemptByAgent = new Map();
+ for (const run of byId.values()) {
+ // Admission order, not finish order: an old attempt can finish late.
+ const createdAt = timestamp(run.createdAt);
+ const order = Number.isFinite(createdAt) ? createdAt : timestamp(run.startedAt);
+ if (Number.isFinite(order)) {
+ latestByAgent.set(run.agentId, Math.max(latestByAgent.get(run.agentId) ?? -Infinity, order));
+ if (!isAdmissionWait(run)) {
+ latestAttemptByAgent.set(run.agentId, Math.max(latestAttemptByAgent.get(run.agentId) ?? -Infinity, order));
+ }
+ }
+ }
+
+ const isHistoricalRun = (runId: string): boolean => {
+ if (terminal) return true;
+ const run = byId.get(runId);
+ if (!run) return false;
+ if (run.execution?.successorRunId || run.execution?.phase === "completed") return true;
+ const wait = run.resultJson?.executionWait;
+ if (recoveryAction && wait && typeof wait === "object" && "recoveryActionId" in wait
+ && wait.recoveryActionId === recoveryAction.id) {
+ return recoveryAction.status === "resolved" || recoveryAction.status === "cancelled";
+ }
+ // A newer admission attempt does not resolve an explicit execution hold.
+ if (run.execution?.phase === "recovery_needed") return false;
+ const createdAt = timestamp(run.createdAt);
+ const order = Number.isFinite(createdAt) ? createdAt : timestamp(run.startedAt);
+ const latest = isAdmissionWait(run) ? latestByAgent : latestAttemptByAgent;
+ return order < (latest.get(run.agentId) ?? -Infinity);
+ };
+
+ const isHistoricalNotice = (comment: IssueChatComment): boolean => {
+ // Session resets are durable conversation boundaries, not execution status.
+ if (comment.conversationSessionGeneration != null) return false;
+ if (comment.authorType !== "system" && comment.presentation?.kind !== "system_notice") return false;
+ const sourceRunId = comment.metadata?.sourceRunId ?? comment.createdByRunId
+ ?? comment.runId ?? comment.derivedCreatedByRunId;
+ const recoveryActionId = comment.metadata?.recovery?.actionId;
+ // System authorship alone does not make a comment execution status. Child
+ // task relays and other unrelated updates have their own unresolved state.
+ // Without run/recovery provenance, a later run cannot prove them obsolete.
+ if (!sourceRunId && !recoveryActionId) return false;
+ if (terminal) return true;
+ if (recoveryActionId === recoveryAction?.id && recoveryAction) {
+ return recoveryAction.status === "resolved" || recoveryAction.status === "cancelled";
+ }
+ if (sourceRunId) return isHistoricalRun(sourceRunId);
+ return false;
+ };
+
+ return { isHistoricalRun, isHistoricalNotice };
+}
+
+/** Remove obsolete lifecycle diagnostics, preserving responses, tools and inputs. */
+export function withoutHistoricalRunStatus(items: readonly TaskChatItem[]): TaskChatItem[] {
+ return items.filter((item) => {
+ if (item.kind === "marker" || item.kind === "status") return false;
+ if (item.kind === "message") return item.author !== "system";
+ if (item.kind !== "protocol") return true;
+ if (item.surface === "run_terminal" || item.surface === "run_result") return false;
+ return item.surface !== "provider_activity"
+ || !["terminal", "wait", "provider_notice"].includes(item.family);
+ });
+}
diff --git a/ui/src/components/task-chat/task-chat-model.ts b/ui/src/components/task-chat/task-chat-model.ts
index f903a17f008..e2919721e1f 100644
--- a/ui/src/components/task-chat/task-chat-model.ts
+++ b/ui/src/components/task-chat/task-chat-model.ts
@@ -508,6 +508,8 @@ export interface TaskChatTurnItem {
kind: "turn";
items: TaskChatTurnChildItem[];
settled: boolean;
+ /** Superseded execution status is omitted; activity remains inspectable. */
+ historical?: boolean;
/** Agent identity retained when a live runner turn becomes durable history. */
agentName?: string;
agentIcon?: string | null;
diff --git a/ui/src/components/task-chat/transcript-adapter.ts b/ui/src/components/task-chat/transcript-adapter.ts
index 999cf904cbf..5f84863768a 100644
--- a/ui/src/components/task-chat/transcript-adapter.ts
+++ b/ui/src/components/task-chat/transcript-adapter.ts
@@ -1959,6 +1959,7 @@ export function coalesceSettledTurns(
held &&
meta &&
heldMeta &&
+ Boolean(item.historical) === Boolean(held.historical) &&
meta.agentKey &&
meta.agentKey === heldMeta.agentKey
) {