From 1cbf96a777f4e933e0e0d55a5b2df6025ddfd962 Mon Sep 17 00:00:00 2001 From: MarcoDotIO Date: Wed, 4 Nov 2026 21:04:17 -0500 Subject: [PATCH 1/4] fix(server): imported Claude threads no longer show background task notifications as user messages Claude Code records each background task completion as a user record whose text is a block. The session importer treated these as user prompts, so imported threads showed the raw XML as user bubbles and a notification could become the derived thread title. Skip them like the other records Claude injects itself. Co-Authored-By: Claude Opus 5.5 --- .../src/project/AgentSessionScanner.test.ts | 67 +++++++++++++++++++ .../server/src/project/AgentSessionScanner.ts | 19 +++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/apps/server/src/project/AgentSessionScanner.test.ts b/apps/server/src/project/AgentSessionScanner.test.ts index 0844e3cd71834..024ecda0e295f 100644 --- a/apps/server/src/project/AgentSessionScanner.test.ts +++ b/apps/server/src/project/AgentSessionScanner.test.ts @@ -2299,6 +2299,73 @@ 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", + origin: { kind: "task-notification" }, + 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}` }, + }), + ].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}` }, + ], + }); + }); + 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..224f0eca92c19 100644 --- a/apps/server/src/project/AgentSessionScanner.ts +++ b/apps/server/src/project/AgentSessionScanner.ts @@ -285,8 +285,25 @@ 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 = ""; + return ( + text.startsWith("") && + 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. From 3e566ff361eb92480dd55865f24a8d269c2c58d8 Mon Sep 17 00:00:00 2001 From: MarcoDotIO Date: Wed, 4 Nov 2026 21:11:51 -0500 Subject: [PATCH 2/4] fix(server): a lone opening task-notification tag is still imported Co-Authored-By: Claude Opus 5.5 --- apps/server/src/project/AgentSessionScanner.test.ts | 6 ++++++ apps/server/src/project/AgentSessionScanner.ts | 1 + 2 files changed, 7 insertions(+) diff --git a/apps/server/src/project/AgentSessionScanner.test.ts b/apps/server/src/project/AgentSessionScanner.test.ts index 024ecda0e295f..1626efe98c507 100644 --- a/apps/server/src/project/AgentSessionScanner.test.ts +++ b/apps/server/src/project/AgentSessionScanner.test.ts @@ -2346,6 +2346,11 @@ describe("parseAgentSessionTranscript", () => { sessionId: "claude-session", message: { role: "user", content: `${notification}\nCompare with\n${notification}` }, }), + JSON.stringify({ + type: "user", + sessionId: "claude-session", + message: { role: "user", content: "" }, + }), ].join("\n"), source: "claudeAgent", providerInstanceId: ProviderInstanceId.make("claudeAgent"), @@ -2362,6 +2367,7 @@ describe("parseAgentSessionTranscript", () => { { 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: "" }, ], }); }); diff --git a/apps/server/src/project/AgentSessionScanner.ts b/apps/server/src/project/AgentSessionScanner.ts index 224f0eca92c19..3ced0c88581b8 100644 --- a/apps/server/src/project/AgentSessionScanner.ts +++ b/apps/server/src/project/AgentSessionScanner.ts @@ -293,6 +293,7 @@ function isClaudeTaskNotification(record: DecodedTranscriptRecord): boolean { const closingTag = ""; return ( text.startsWith("") && + text.endsWith(closingTag) && text.indexOf(closingTag) === text.length - closingTag.length ); } From 7f59d6dd6d1073cde709bb222e19c23a020fb56c Mon Sep 17 00:00:00 2001 From: MarcoDotIO Date: Wed, 4 Nov 2026 21:18:31 -0500 Subject: [PATCH 3/4] fix(server): a task notification pasted after the user's own text is still imported Co-Authored-By: Claude Opus 5.5 --- apps/server/src/project/AgentSessionScanner.test.ts | 6 ++++++ apps/server/src/project/AgentSessionScanner.ts | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/server/src/project/AgentSessionScanner.test.ts b/apps/server/src/project/AgentSessionScanner.test.ts index 1626efe98c507..ecb44001c5d61 100644 --- a/apps/server/src/project/AgentSessionScanner.test.ts +++ b/apps/server/src/project/AgentSessionScanner.test.ts @@ -2351,6 +2351,11 @@ describe("parseAgentSessionTranscript", () => { 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"), @@ -2368,6 +2373,7 @@ describe("parseAgentSessionTranscript", () => { { 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}` }, ], }); }); diff --git a/apps/server/src/project/AgentSessionScanner.ts b/apps/server/src/project/AgentSessionScanner.ts index 3ced0c88581b8..1e2dfbd106fb1 100644 --- a/apps/server/src/project/AgentSessionScanner.ts +++ b/apps/server/src/project/AgentSessionScanner.ts @@ -291,8 +291,9 @@ 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.startsWith("") && + text.lastIndexOf("") === 0 && text.endsWith(closingTag) && text.indexOf(closingTag) === text.length - closingTag.length ); From a263350f1c23798c19da50094476d9599c6d56bc Mon Sep 17 00:00:00 2001 From: MarcoDotIO Date: Wed, 4 Nov 2026 21:28:08 -0500 Subject: [PATCH 4/4] test(server): cover a task notification written without origin Co-Authored-By: Claude Opus 5.5 --- apps/server/src/project/AgentSessionScanner.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/server/src/project/AgentSessionScanner.test.ts b/apps/server/src/project/AgentSessionScanner.test.ts index ecb44001c5d61..420889965e546 100644 --- a/apps/server/src/project/AgentSessionScanner.test.ts +++ b/apps/server/src/project/AgentSessionScanner.test.ts @@ -2328,7 +2328,6 @@ describe("parseAgentSessionTranscript", () => { JSON.stringify({ type: "user", sessionId: "claude-session", - origin: { kind: "task-notification" }, message: { role: "user", content: notification }, }), JSON.stringify({