diff --git a/hooks/session-start-seen-skills.mjs b/hooks/session-start-seen-skills.mjs index 07736591..0aabc23e 100644 --- a/hooks/session-start-seen-skills.mjs +++ b/hooks/session-start-seen-skills.mjs @@ -11,7 +11,7 @@ import { removeAllSessionDedupArtifacts } from "./hook-env.mjs"; import { createLogger } from "./logger.mjs"; -var CONTEXT_CLEARING_EVENTS = /* @__PURE__ */ new Set(["clear", "compact"]); +var CONTEXT_CLEARING_SOURCES = /* @__PURE__ */ new Set(["clear", "compact"]); function parseSessionStartSeenSkillsInput(raw) { try { if (!raw.trim()) return null; @@ -44,9 +44,9 @@ function main() { process.stdout.write(formatSessionStartSeenSkillsCursorOutput()); return; } - const hookEvent = input?.hook_event_name ?? ""; + const source = input?.source ?? ""; const sessionId = input?.session_id ?? ""; - const resetTriggered = CONTEXT_CLEARING_EVENTS.has(hookEvent) && !!sessionId; + const resetTriggered = CONTEXT_CLEARING_SOURCES.has(source) && !!sessionId; let removedFiles = 0; let removedDirs = 0; if (resetTriggered) { @@ -55,7 +55,7 @@ function main() { removedDirs = result.removedDirs; } log.debug("session-start-seen-skills:decision", { - event: hookEvent || "unknown", + source: source || "unknown", sessionId: sessionId || "none", resetTriggered, removedFiles, diff --git a/hooks/src/session-start-seen-skills.mts b/hooks/src/session-start-seen-skills.mts index 3e7f4c46..25b7823f 100644 --- a/hooks/src/session-start-seen-skills.mts +++ b/hooks/src/session-start-seen-skills.mts @@ -29,11 +29,12 @@ interface SessionStartSeenSkillsInput { conversation_id?: string; cursor_version?: string; hook_event_name?: string; + source?: string; [key: string]: unknown; } -/** Events where previously-injected skills are no longer in the context window. */ -const CONTEXT_CLEARING_EVENTS = new Set(["clear", "compact"]); +/** Sources where previously-injected skills are no longer in the context window. */ +const CONTEXT_CLEARING_SOURCES = new Set(["clear", "compact"]); export function parseSessionStartSeenSkillsInput(raw: string): SessionStartSeenSkillsInput | null { try { @@ -84,9 +85,9 @@ function main(): void { } // Claude Code: reset dedup state on clear/compact so skills get re-injected. - const hookEvent = input?.hook_event_name ?? ""; + const source = input?.source ?? ""; const sessionId = input?.session_id ?? ""; - const resetTriggered = CONTEXT_CLEARING_EVENTS.has(hookEvent) && !!sessionId; + const resetTriggered = CONTEXT_CLEARING_SOURCES.has(source) && !!sessionId; let removedFiles = 0; let removedDirs = 0; @@ -98,7 +99,7 @@ function main(): void { } log.debug("session-start-seen-skills:decision", { - event: hookEvent || "unknown", + source: source || "unknown", sessionId: sessionId || "none", resetTriggered, removedFiles, diff --git a/tests/session-start-seen-skills.test.ts b/tests/session-start-seen-skills.test.ts index dbbd2078..88d77da4 100644 --- a/tests/session-start-seen-skills.test.ts +++ b/tests/session-start-seen-skills.test.ts @@ -133,7 +133,7 @@ describe("session-start-seen-skills hook", () => { // Fire the hook with a "clear" event const result = await runSessionStart( { CLAUDE_ENV_FILE: undefined }, - JSON.stringify({ session_id: sessionId, hook_event_name: "clear" }), + JSON.stringify({ session_id: sessionId, hook_event_name: "SessionStart", source: "clear" }), ); expect(result.code).toBe(0); @@ -161,7 +161,7 @@ describe("session-start-seen-skills hook", () => { const result = await runSessionStart( { CLAUDE_ENV_FILE: undefined }, - JSON.stringify({ session_id: sessionId, hook_event_name: "compact" }), + JSON.stringify({ session_id: sessionId, hook_event_name: "SessionStart", source: "compact" }), ); expect(result.code).toBe(0); @@ -187,7 +187,7 @@ describe("session-start-seen-skills hook", () => { // 3. Fire "clear" event via session-start hook const result = await runSessionStart( { CLAUDE_ENV_FILE: undefined }, - JSON.stringify({ session_id: sessionId, hook_event_name: "clear" }), + JSON.stringify({ session_id: sessionId, hook_event_name: "SessionStart", source: "clear" }), ); expect(result.code).toBe(0); @@ -213,7 +213,7 @@ describe("session-start-seen-skills hook", () => { // 3. Fire "compact" event via session-start hook const result = await runSessionStart( { CLAUDE_ENV_FILE: undefined }, - JSON.stringify({ session_id: sessionId, hook_event_name: "compact" }), + JSON.stringify({ session_id: sessionId, hook_event_name: "SessionStart", source: "compact" }), ); expect(result.code).toBe(0); @@ -253,7 +253,7 @@ describe("session-start-seen-skills hook", () => { // Fire "clear" event const result = await runSessionStart( { CLAUDE_ENV_FILE: undefined }, - JSON.stringify({ session_id: sessionId, hook_event_name: "clear" }), + JSON.stringify({ session_id: sessionId, hook_event_name: "SessionStart", source: "clear" }), ); expect(result.code).toBe(0); @@ -291,7 +291,7 @@ describe("session-start-seen-skills hook", () => { const result = await runSessionStart( { CLAUDE_ENV_FILE: undefined }, - JSON.stringify({ session_id: sessionId, hook_event_name: "startup" }), + JSON.stringify({ session_id: sessionId, hook_event_name: "SessionStart", source: "startup" }), ); expect(result.code).toBe(0); @@ -304,6 +304,62 @@ describe("session-start-seen-skills hook", () => { try { rmSync(dedupFilePath(sessionId, "seen-skills")); } catch {} } }); + + test("test_clear_event_preserves_other_session_artifacts", async () => { + const sessionId = crypto.randomUUID(); + const otherSessionId = crypto.randomUUID(); + + try { + expect(tryClaimSessionKey(sessionId, "seen-skills", "nextjs")).toBe(true); + expect(tryClaimSessionKey(otherSessionId, "seen-skills", "nextjs")).toBe(true); + expect(tryClaimSessionKey(otherSessionId, "seen-skills", "ai-sdk", "subagent-abc")).toBe(true); + writeFileSync(dedupFilePath(otherSessionId, "seen-skills"), "nextjs", "utf-8"); + + const result = await runSessionStart( + { CLAUDE_ENV_FILE: undefined }, + JSON.stringify({ session_id: sessionId, hook_event_name: "SessionStart", source: "clear" }), + ); + + expect(result.code).toBe(0); + expect(existsSync(dedupClaimDirPath(sessionId, "seen-skills"))).toBe(false); + expect(existsSync(dedupClaimDirPath(otherSessionId, "seen-skills"))).toBe(true); + expect(existsSync(dedupClaimDirPath(otherSessionId, "seen-skills", "subagent-abc"))).toBe(true); + expect(readFileSync(dedupFilePath(otherSessionId, "seen-skills"), "utf-8")).toBe("nextjs"); + } finally { + removeAllSessionDedupArtifacts(sessionId); + removeAllSessionDedupArtifacts(otherSessionId); + } + }); + + const preservingPayloads: Array<[string, (sessionId: string) => Record]> = [ + ["resume", (sessionId) => ({ session_id: sessionId, hook_event_name: "SessionStart", source: "resume" })], + ["unknown source", (sessionId) => ({ session_id: sessionId, hook_event_name: "SessionStart", source: "fork" })], + ["missing source", (sessionId) => ({ session_id: sessionId, hook_event_name: "SessionStart" })], + ["legacy hook_event_name clear", (sessionId) => ({ session_id: sessionId, hook_event_name: "clear" })], + ["legacy hook_event_name compact", (sessionId) => ({ session_id: sessionId, hook_event_name: "compact" })], + ["missing session_id", () => ({ hook_event_name: "SessionStart", source: "clear" })], + ["empty session_id", () => ({ session_id: "", hook_event_name: "SessionStart", source: "compact" })], + ["cursor payload", (sessionId) => ({ session_id: sessionId, conversation_id: "cursor-conversation", source: "clear" })], + ]; + + for (const [name, payload] of preservingPayloads) { + test(`test_${name.replaceAll(" ", "_")}_preserves_claim_dir`, async () => { + const sessionId = crypto.randomUUID(); + + try { + expect(tryClaimSessionKey(sessionId, "seen-skills", "nextjs")).toBe(true); + writeFileSync(dedupFilePath(sessionId, "seen-skills"), "nextjs", "utf-8"); + + const result = await runSessionStart({ CLAUDE_ENV_FILE: undefined }, JSON.stringify(payload(sessionId))); + + expect(result.code).toBe(0); + expect(existsSync(dedupClaimDirPath(sessionId, "seen-skills"))).toBe(true); + expect(readFileSync(dedupFilePath(sessionId, "seen-skills"), "utf-8")).toBe("nextjs"); + } finally { + removeAllSessionDedupArtifacts(sessionId); + } + }); + } }); describe("hook-env session temp path guards", () => {