mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-22 18:13:41 +08:00
Merge pull request #1495 from ratorin/fix/session-end-transcript-path-isolation
fix(hooks): isolate session-end.js filename using transcript_path UUID (#1494)
This commit is contained in:
@@ -16,6 +16,7 @@ const {
|
|||||||
getDateString,
|
getDateString,
|
||||||
getTimeString,
|
getTimeString,
|
||||||
getSessionIdShort,
|
getSessionIdShort,
|
||||||
|
sanitizeSessionId,
|
||||||
getProjectName,
|
getProjectName,
|
||||||
ensureDir,
|
ensureDir,
|
||||||
readFile,
|
readFile,
|
||||||
@@ -178,19 +179,45 @@ function mergeSessionHeader(content, today, currentTime, metadata) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Parse stdin JSON to get transcript_path
|
// Parse stdin JSON to get transcript_path; fall back to env var on missing,
|
||||||
|
// empty, or non-string values as well as on malformed JSON.
|
||||||
let transcriptPath = null;
|
let transcriptPath = null;
|
||||||
try {
|
try {
|
||||||
const input = JSON.parse(stdinData);
|
const input = JSON.parse(stdinData);
|
||||||
transcriptPath = input.transcript_path;
|
if (input && typeof input.transcript_path === 'string' && input.transcript_path.length > 0) {
|
||||||
|
transcriptPath = input.transcript_path;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback: try env var for backwards compatibility
|
// Malformed stdin: fall through to the env-var fallback below.
|
||||||
transcriptPath = process.env.CLAUDE_TRANSCRIPT_PATH;
|
}
|
||||||
|
if (!transcriptPath) {
|
||||||
|
const envTranscriptPath = process.env.CLAUDE_TRANSCRIPT_PATH;
|
||||||
|
if (typeof envTranscriptPath === 'string' && envTranscriptPath.length > 0) {
|
||||||
|
transcriptPath = envTranscriptPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionsDir = getSessionsDir();
|
const sessionsDir = getSessionsDir();
|
||||||
const today = getDateString();
|
const today = getDateString();
|
||||||
const shortId = getSessionIdShort();
|
// Derive shortId from transcript_path UUID when available, using the SAME
|
||||||
|
// last-8-chars convention as getSessionIdShort(sessionId.slice(-8)). This keeps
|
||||||
|
// backward compatibility for normal sessions (the derived shortId matches what
|
||||||
|
// getSessionIdShort() would have produced from the same UUID), while making
|
||||||
|
// every session map to a unique filename based on its own transcript UUID.
|
||||||
|
//
|
||||||
|
// Without this, a parent session and any `claude -p ...` subprocess spawned by
|
||||||
|
// another Stop hook share the project-name fallback filename, and the subprocess
|
||||||
|
// overwrites the parent's summary. See issue #1494 for full repro details.
|
||||||
|
let shortId = null;
|
||||||
|
if (transcriptPath) {
|
||||||
|
const m = path.basename(transcriptPath).match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i);
|
||||||
|
if (m) {
|
||||||
|
// Run through sanitizeSessionId() for byte-for-byte parity with
|
||||||
|
// getSessionIdShort(sessionId.slice(-8)).
|
||||||
|
shortId = sanitizeSessionId(m[1].slice(-8).toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!shortId) { shortId = getSessionIdShort(); }
|
||||||
const sessionFile = path.join(sessionsDir, `${today}-${shortId}-session.tmp`);
|
const sessionFile = path.join(sessionsDir, `${today}-${shortId}-session.tmp`);
|
||||||
const sessionMetadata = getSessionMetadata();
|
const sessionMetadata = getSessionMetadata();
|
||||||
|
|
||||||
|
|||||||
@@ -651,6 +651,114 @@ async function runTests() {
|
|||||||
passed++;
|
passed++;
|
||||||
else failed++;
|
else failed++;
|
||||||
|
|
||||||
|
// Regression test for #1494: transcript_path UUID-derived shortId (last 8 chars)
|
||||||
|
// isolates sibling subprocess invocations while preserving getSessionIdShort()
|
||||||
|
// backward compatibility (same `.slice(-8)` convention).
|
||||||
|
if (
|
||||||
|
await asyncTest('derives shortId from transcript_path UUID when available', async () => {
|
||||||
|
const isoHome = path.join(os.tmpdir(), `ecc-session-transcript-${Date.now()}`);
|
||||||
|
const transcriptUuid = 'abcdef12-3456-4789-a012-bcdef3456789';
|
||||||
|
const expectedShortId = 'f3456789'; // Last 8 chars of UUID (matches getSessionIdShort convention)
|
||||||
|
const transcriptPath = path.join(isoHome, 'transcripts', `${transcriptUuid}.jsonl`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(transcriptPath), { recursive: true });
|
||||||
|
fs.writeFileSync(transcriptPath, '');
|
||||||
|
|
||||||
|
const stdinJson = JSON.stringify({ transcript_path: transcriptPath });
|
||||||
|
await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson, {
|
||||||
|
HOME: isoHome,
|
||||||
|
USERPROFILE: isoHome,
|
||||||
|
// Clear CLAUDE_SESSION_ID so parent-process env does not leak into the
|
||||||
|
// child and the test deterministically exercises the transcript_path
|
||||||
|
// branch (getSessionIdShort() is the alternative path that is not
|
||||||
|
// exercised here).
|
||||||
|
CLAUDE_SESSION_ID: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionsDir = getCanonicalSessionsDir(isoHome);
|
||||||
|
const now = new Date();
|
||||||
|
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||||||
|
const sessionFile = path.join(sessionsDir, `${today}-${expectedShortId}-session.tmp`);
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(sessionFile), `Session file with transcript UUID shortId should exist: ${sessionFile}`);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(isoHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
passed++;
|
||||||
|
else failed++;
|
||||||
|
|
||||||
|
// Regression test for #1494: uppercase UUID hex digits should be normalized to
|
||||||
|
// lowercase so the filename is consistent with getSessionIdShort()'s output.
|
||||||
|
if (
|
||||||
|
await asyncTest('normalizes transcript UUID shortId to lowercase', async () => {
|
||||||
|
const isoHome = path.join(os.tmpdir(), `ecc-session-transcript-upper-${Date.now()}`);
|
||||||
|
const transcriptUuid = 'ABCDEF12-3456-4789-A012-BCDEF3456789';
|
||||||
|
const expectedShortId = 'f3456789'; // last 8 lowercased
|
||||||
|
const transcriptPath = path.join(isoHome, 'transcripts', `${transcriptUuid}.jsonl`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(transcriptPath), { recursive: true });
|
||||||
|
fs.writeFileSync(transcriptPath, '');
|
||||||
|
|
||||||
|
const stdinJson = JSON.stringify({ transcript_path: transcriptPath });
|
||||||
|
await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson, {
|
||||||
|
HOME: isoHome,
|
||||||
|
USERPROFILE: isoHome,
|
||||||
|
CLAUDE_SESSION_ID: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionsDir = getCanonicalSessionsDir(isoHome);
|
||||||
|
const now = new Date();
|
||||||
|
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||||||
|
const sessionFile = path.join(sessionsDir, `${today}-${expectedShortId}-session.tmp`);
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(sessionFile), `Session file with lowercase shortId should exist: ${sessionFile}`);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(isoHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
passed++;
|
||||||
|
else failed++;
|
||||||
|
|
||||||
|
// Regression test for #1494: when CLAUDE_SESSION_ID and transcript_path refer to the
|
||||||
|
// same UUID, the derived shortId must be identical to the pre-fix behaviour so that
|
||||||
|
// existing .tmp files are not orphaned on upgrade.
|
||||||
|
if (
|
||||||
|
await asyncTest('matches getSessionIdShort when transcript UUID equals CLAUDE_SESSION_ID', async () => {
|
||||||
|
const isoHome = path.join(os.tmpdir(), `ecc-session-transcript-match-${Date.now()}`);
|
||||||
|
const sessionUuid = '11223344-5566-4778-8899-aabbccddeeff';
|
||||||
|
const expectedShortId = 'ccddeeff'; // last 8 chars of both transcript UUID and CLAUDE_SESSION_ID
|
||||||
|
const transcriptPath = path.join(isoHome, 'transcripts', `${sessionUuid}.jsonl`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(transcriptPath), { recursive: true });
|
||||||
|
fs.writeFileSync(transcriptPath, '');
|
||||||
|
|
||||||
|
const stdinJson = JSON.stringify({ transcript_path: transcriptPath });
|
||||||
|
await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson, {
|
||||||
|
HOME: isoHome,
|
||||||
|
USERPROFILE: isoHome,
|
||||||
|
CLAUDE_SESSION_ID: sessionUuid
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionsDir = getCanonicalSessionsDir(isoHome);
|
||||||
|
const now = new Date();
|
||||||
|
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||||||
|
const sessionFile = path.join(sessionsDir, `${today}-${expectedShortId}-session.tmp`);
|
||||||
|
|
||||||
|
assert.ok(fs.existsSync(sessionFile), `Session filename should match the pre-fix CLAUDE_SESSION_ID-based name: ${sessionFile}`);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(isoHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
passed++;
|
||||||
|
else failed++;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
await asyncTest('writes project, branch, and worktree metadata into new session files', async () => {
|
await asyncTest('writes project, branch, and worktree metadata into new session files', async () => {
|
||||||
const isoHome = path.join(os.tmpdir(), `ecc-session-metadata-${Date.now()}`);
|
const isoHome = path.join(os.tmpdir(), `ecc-session-metadata-${Date.now()}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user