keep background task notifications out of the transcript
The harness delivers a finished background task to the agent by writing it as the user's next message, so Claude's file holds a raw <task-notification> envelope as a user turn. Live it never shows, because the same event travels separately as task:notification — it appeared only when a refresh rebuilt the conversation from the file, as a bubble on the owner's side he never typed. Same defect as INTERRUPTION_MARKERS and the same fix. Anchored to the start of the message so quoting one inside a real message stays yours. Also skipped when picking a session's title, where it is no more a title than a slash command is. Verified against a live transcript: 38 user bubbles before, 32 after, the 6 removed being exactly the notifications. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -178,7 +178,11 @@ function summarizeTranscript(filePath: string, id: string): TranscriptSummary |
|
|||||||
sawFirstUserEntry = true;
|
sawFirstUserEntry = true;
|
||||||
bornFromClear = text.startsWith('<command-name>/clear</command-name>');
|
bornFromClear = text.startsWith('<command-name>/clear</command-name>');
|
||||||
}
|
}
|
||||||
if (text && !firstUserText && !text.startsWith('<command-name>')) firstUserText = text;
|
// A resumed session can open with a background task reporting in, which is no more a title than a
|
||||||
|
// slash command is — see `isTaskNotification`.
|
||||||
|
if (text && !firstUserText && !text.startsWith('<command-name>') && !isTaskNotification(text)) {
|
||||||
|
firstUserText = text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,8 +388,23 @@ function blockText(content: unknown): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const userOrInterruption = (text: string): ClaudeChatMessage =>
|
/**
|
||||||
INTERRUPTION_MARKERS.has(text.trim()) ? { role: 'interrupted' } : { role: 'user', text };
|
* A background task reporting in. The harness delivers these to the agent by writing them as the *user's*
|
||||||
|
* next message — the same trick as `INTERRUPTION_MARKERS`, and the same consequence: replayed literally,
|
||||||
|
* the transcript shows a raw XML envelope in a bubble on your side of the conversation that you never
|
||||||
|
* typed. Live it never appears, because a finished task travels as its own `task:notification` event; it
|
||||||
|
* surfaces only when a refresh rebuilds the conversation from Claude's file.
|
||||||
|
*
|
||||||
|
* Anchored to the start of the message rather than matched anywhere in it, so quoting one inside a real
|
||||||
|
* message — this file's own conversation does exactly that — stays yours.
|
||||||
|
*/
|
||||||
|
const isTaskNotification = (text: string) => text.trimStart().startsWith('<task-notification>');
|
||||||
|
|
||||||
|
function userMessageFrom(text: string): ClaudeChatMessage | null {
|
||||||
|
if (isTaskNotification(text)) return null;
|
||||||
|
if (INTERRUPTION_MARKERS.has(text.trim())) return { role: 'interrupted' };
|
||||||
|
return { role: 'user', text };
|
||||||
|
}
|
||||||
|
|
||||||
export type ClaudeSessionDetail = { id: string; model: string; cwd: string; messages: ClaudeChatMessage[] };
|
export type ClaudeSessionDetail = { id: string; model: string; cwd: string; messages: ClaudeChatMessage[] };
|
||||||
|
|
||||||
@@ -430,13 +449,15 @@ function parseClaudeTranscript(filePath: string, sessionId: string, fallbackCwd
|
|||||||
|
|
||||||
if (entry.type === 'user' && !entry.isMeta) {
|
if (entry.type === 'user' && !entry.isMeta) {
|
||||||
if (typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
if (content.trim()) messages.push(userOrInterruption(content));
|
const message = content.trim() ? userMessageFrom(content) : null;
|
||||||
|
if (message) messages.push(message);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
for (const block of content as ContentBlock[]) {
|
for (const block of content as ContentBlock[]) {
|
||||||
if (block.type === 'text' && block.text?.trim()) {
|
if (block.type === 'text' && block.text?.trim()) {
|
||||||
messages.push(userOrInterruption(block.text));
|
const message = userMessageFrom(block.text);
|
||||||
|
if (message) messages.push(message);
|
||||||
} else if (block.type === 'tool_result') {
|
} else if (block.type === 'tool_result') {
|
||||||
const tool = toolById.get(block.tool_use_id);
|
const tool = toolById.get(block.tool_use_id);
|
||||||
if (tool) {
|
if (tool) {
|
||||||
|
|||||||
Reference in New Issue
Block a user