1fac57535e
When Claude Code runs in a worktree (via Agent tool with isolation: "worktree"), the transcript path points to the worktree's project directory. After the worktree is cleaned up, the Stop hook fires but the transcript file no longer exists, causing extractLastMessage() to throw. This error triggers Claude to respond, which fires another Stop hook, creating an infinite error loop. Changed throws to warn-and-return-empty so the summarize hook exits cleanly with exit 0 instead of cascading errors. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
68 lines
2.0 KiB
TypeScript
68 lines
2.0 KiB
TypeScript
import { readFileSync, existsSync } from 'fs';
|
|
import { logger } from '../utils/logger.js';
|
|
|
|
/**
|
|
* Extract last message of specified role from transcript JSONL file
|
|
* @param transcriptPath Path to transcript file
|
|
* @param role 'user' or 'assistant'
|
|
* @param stripSystemReminders Whether to remove <system-reminder> tags (for assistant)
|
|
*/
|
|
export function extractLastMessage(
|
|
transcriptPath: string,
|
|
role: 'user' | 'assistant',
|
|
stripSystemReminders: boolean = false
|
|
): string {
|
|
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
logger.warn('PARSER', `Transcript path missing or file does not exist: ${transcriptPath}`);
|
|
return '';
|
|
}
|
|
|
|
const content = readFileSync(transcriptPath, 'utf-8').trim();
|
|
if (!content) {
|
|
logger.warn('PARSER', `Transcript file exists but is empty: ${transcriptPath}`);
|
|
return '';
|
|
}
|
|
|
|
const lines = content.split('\n');
|
|
let foundMatchingRole = false;
|
|
|
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
const line = JSON.parse(lines[i]);
|
|
if (line.type === role) {
|
|
foundMatchingRole = true;
|
|
|
|
if (line.message?.content) {
|
|
let text = '';
|
|
const msgContent = line.message.content;
|
|
|
|
if (typeof msgContent === 'string') {
|
|
text = msgContent;
|
|
} else if (Array.isArray(msgContent)) {
|
|
text = msgContent
|
|
.filter((c: any) => c.type === 'text')
|
|
.map((c: any) => c.text)
|
|
.join('\n');
|
|
} else {
|
|
// Unknown content format - throw error
|
|
throw new Error(`Unknown message content format in transcript. Type: ${typeof msgContent}`);
|
|
}
|
|
|
|
if (stripSystemReminders) {
|
|
text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '');
|
|
text = text.replace(/\n{3,}/g, '\n\n').trim();
|
|
}
|
|
|
|
// Return text even if empty - caller decides if that's an error
|
|
return text;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we searched the whole transcript and didn't find any message of this role
|
|
if (!foundMatchingRole) {
|
|
return '';
|
|
}
|
|
|
|
return '';
|
|
}
|