diff --git a/apps/server/src/project/AgentSessionScanner.test.ts b/apps/server/src/project/AgentSessionScanner.test.ts
index 0844e3cd71834..420889965e546 100644
--- a/apps/server/src/project/AgentSessionScanner.test.ts
+++ b/apps/server/src/project/AgentSessionScanner.test.ts
@@ -2299,6 +2299,84 @@ describe("parseAgentSessionTranscript", () => {
});
});
+ it("drops Claude background task notifications injected as user turns", () => {
+ const notification = [
+ "",
+ "task-1",
+ "completed",
+ 'Background command "Run tests" completed',
+ "",
+ ].join("\n");
+ const thread = AgentSessionScanner.parseAgentSessionTranscript({
+ contents: [
+ JSON.stringify({
+ type: "user",
+ sessionId: "claude-session",
+ origin: { kind: "task-notification" },
+ message: { role: "user", content: notification },
+ }),
+ JSON.stringify({
+ type: "user",
+ sessionId: "claude-session",
+ message: { role: "user", content: "Run the tests in the background" },
+ }),
+ JSON.stringify({
+ type: "assistant",
+ sessionId: "claude-session",
+ message: { role: "assistant", content: [{ type: "text", text: "Started the tests" }] },
+ }),
+ JSON.stringify({
+ type: "user",
+ sessionId: "claude-session",
+ message: { role: "user", content: notification },
+ }),
+ JSON.stringify({
+ type: "assistant",
+ sessionId: "claude-session",
+ message: { role: "assistant", content: [{ type: "text", text: "All tests passed" }] },
+ }),
+ JSON.stringify({
+ type: "user",
+ sessionId: "claude-session",
+ message: { role: "user", content: "Why does show up in logs?" },
+ }),
+ JSON.stringify({
+ type: "user",
+ sessionId: "claude-session",
+ message: { role: "user", content: `${notification}\nCompare with\n${notification}` },
+ }),
+ JSON.stringify({
+ type: "user",
+ sessionId: "claude-session",
+ message: { role: "user", content: "" },
+ }),
+ JSON.stringify({
+ type: "user",
+ sessionId: "claude-session",
+ message: { role: "user", content: ` is in my logs:\n${notification}` },
+ }),
+ ].join("\n"),
+ source: "claudeAgent",
+ providerInstanceId: ProviderInstanceId.make("claudeAgent"),
+ fallbackSessionId: "fallback",
+ lastActiveAtMs: Date.parse("2026-08-24T12:00:00.000Z"),
+ });
+
+ expect(thread).toMatchObject({
+ title: "Run the tests in the background",
+ omittedMessageCount: 0,
+ messages: [
+ { role: "user", text: "Run the tests in the background" },
+ { role: "assistant", text: "Started the tests" },
+ { role: "assistant", text: "All tests passed" },
+ { role: "user", text: "Why does show up in logs?" },
+ { role: "user", text: `${notification}\nCompare with\n${notification}` },
+ { role: "user", text: "" },
+ { role: "user", text: ` is in my logs:\n${notification}` },
+ ],
+ });
+ });
+
it.each([
{ customTitles: ["Refactor auth module"], expected: "Refactor auth module" },
// Claude writes an empty customTitle when the user clears a rename.
diff --git a/apps/server/src/project/AgentSessionScanner.ts b/apps/server/src/project/AgentSessionScanner.ts
index 3ed97f8a7d8f4..1e2dfbd106fb1 100644
--- a/apps/server/src/project/AgentSessionScanner.ts
+++ b/apps/server/src/project/AgentSessionScanner.ts
@@ -285,8 +285,27 @@ function codexTurnId(metadata: unknown): string | null {
return decoded.value.turn_id;
}
+// Claude records background task completions as user turns it injected itself; its UI
+// shows them as task status, not as something the user typed.
+function isClaudeTaskNotification(record: DecodedTranscriptRecord): boolean {
+ if (record.type !== "user") return false;
+ const text = extractText(record.message?.content);
+ const closingTag = "";
+ // Exactly one block: user text around a pasted block keeps the message.
+ return (
+ text.lastIndexOf("") === 0 &&
+ text.endsWith(closingTag) &&
+ text.indexOf(closingTag) === text.length - closingTag.length
+ );
+}
+
function isHiddenClaudeRecord(record: DecodedTranscriptRecord): boolean {
- return record.isSidechain === true || record.isMeta === true || record.isCompactSummary === true;
+ return (
+ record.isSidechain === true ||
+ record.isMeta === true ||
+ record.isCompactSummary === true ||
+ isClaudeTaskNotification(record)
+ );
}
// Claude repeats session fields on records that need not be retained.