fix(reminder-hooks): preserve suppression state across compaction

This commit is contained in:
Disaster-Terminator
2026-04-18 09:40:09 +08:00
committed by YeonGyu-Kim
parent ae7ff3bb7e
commit 3db1da1e5b
4 changed files with 108 additions and 16 deletions
+3 -7
View File
@@ -42,6 +42,8 @@ const ORCHESTRATOR_AGENTS = new Set([
"prometheus",
]);
const MAX_REMINDERS = 3;
function isOrchestratorAgent(agentName: string): boolean {
return ORCHESTRATOR_AGENTS.has(getAgentConfigKey(agentName));
}
@@ -99,7 +101,7 @@ export function createAgentUsageReminderHook(_ctx: PluginInput) {
const state = getOrCreateState(sessionID);
if (state.agentUsed) {
if (state.agentUsed || state.reminderCount >= MAX_REMINDERS) {
return;
}
@@ -119,12 +121,6 @@ export function createAgentUsageReminderHook(_ctx: PluginInput) {
}
}
if (event.type === "session.compacted") {
const sessionID = resolveSessionEventID(props);
if (sessionID) {
resetState(sessionID);
}
}
};
return {
@@ -0,0 +1,80 @@
import type { PluginInput } from "@opencode-ai/plugin";
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
import { createAgentUsageReminderHook } from "./index";
import { clearSessionAgent, updateSessionAgent, _resetForTesting } from "../../features/claude-code-session-state";
import * as storage from "./storage";
describe("agent-usage-reminder hook", () => {
let loadStateSpy: ReturnType<typeof spyOn>;
let saveStateSpy: ReturnType<typeof spyOn>;
let clearStateSpy: ReturnType<typeof spyOn>;
beforeEach(() => {
_resetForTesting();
loadStateSpy = spyOn(storage, "loadAgentUsageState").mockReturnValue(null);
saveStateSpy = spyOn(storage, "saveAgentUsageState").mockImplementation(mock(() => {}));
clearStateSpy = spyOn(storage, "clearAgentUsageState").mockImplementation(mock(() => {}));
});
afterEach(() => {
loadStateSpy?.mockRestore();
saveStateSpy?.mockRestore();
clearStateSpy?.mockRestore();
});
function createHook() {
return createAgentUsageReminderHook({} as PluginInput);
}
test("caps reminders and does not re-arm after session.compacted", async () => {
// given - an orchestrator session has already hit the reminder cap
const hook = createHook();
const sessionID = "agent-usage-compact-session";
updateSessionAgent(sessionID, "Sisyphus");
const output1 = { title: "", output: "result-1", metadata: {} };
const output2 = { title: "", output: "result-2", metadata: {} };
const output3 = { title: "", output: "result-3", metadata: {} };
const output4 = { title: "", output: "result-4", metadata: {} };
await hook["tool.execute.after"]({ tool: "grep", sessionID, callID: "1" }, output1);
await hook["tool.execute.after"]({ tool: "grep", sessionID, callID: "2" }, output2);
await hook["tool.execute.after"]({ tool: "grep", sessionID, callID: "3" }, output3);
// then - the first three reminders are shown
expect(output1.output).toContain("[Agent Usage Reminder]");
expect(output2.output).toContain("[Agent Usage Reminder]");
expect(output3.output).toContain("[Agent Usage Reminder]");
// when - compaction happens and another target tool runs
await hook.event({ event: { type: "session.compacted", properties: { sessionID } } });
await hook["tool.execute.after"]({ tool: "grep", sessionID, callID: "4" }, output4);
// then - compaction does not reset the reminder cap
expect(output4.output).not.toContain("[Agent Usage Reminder]");
clearSessionAgent(sessionID);
});
test("resets reminder state on session.deleted", async () => {
// given - an orchestrator session has reminder state
const hook = createHook();
const sessionID = "agent-usage-delete-session";
updateSessionAgent(sessionID, "Sisyphus");
const output1 = { title: "", output: "result-1", metadata: {} };
const output2 = { title: "", output: "result-2", metadata: {} };
await hook["tool.execute.after"]({ tool: "grep", sessionID, callID: "1" }, output1);
expect(output1.output).toContain("[Agent Usage Reminder]");
// when - the session is deleted and another target tool runs
await hook.event({ event: { type: "session.deleted", properties: { info: { id: sessionID } } } });
await hook["tool.execute.after"]({ tool: "grep", sessionID, callID: "2" }, output2);
// then - deletion still resets the state
expect(output2.output).toContain("[Agent Usage Reminder]");
clearSessionAgent(sessionID);
});
});
@@ -127,12 +127,6 @@ export function createCategorySkillReminderHook(
}
}
if (event.type === "session.compacted") {
const sessionID = resolveSessionEventID(props)
if (sessionID) {
sessionStates.delete(sessionID)
}
}
}
return {
@@ -282,7 +282,7 @@ describe("category-skill-reminder hook", () => {
clearSessionAgent(sessionID)
})
test("should reset state on session.compacted event", async () => {
test("should preserve suppression state on session.compacted event", async () => {
// given - sisyphus agent with reminder already shown
const hook = createHook()
const sessionID = "compact-session"
@@ -302,8 +302,30 @@ describe("category-skill-reminder hook", () => {
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "5" }, output2)
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "6" }, output2)
// then - reminder should be shown again (state was reset)
expect(output2.output).toContain("[Category+Skill Reminder]")
// then - reminder should NOT be shown again (state remains suppressed)
expect(output2.output).not.toContain("[Category+Skill Reminder]")
clearSessionAgent(sessionID)
})
test("should preserve partial tool-call count across session.compacted", async () => {
// given - sisyphus agent with 2 delegatable tool calls
const hook = createHook()
const sessionID = "compact-partial-count-session"
updateSessionAgent(sessionID, "Sisyphus")
const output = { title: "", output: "result", metadata: {} }
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "1" }, output)
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2" }, output)
expect(output.output).not.toContain("[Category+Skill Reminder]")
// when - the session compacts before the third tool call
await hook.event({ event: { type: "session.compacted", properties: { sessionID } } })
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3" }, output)
// then - the third call should still trigger the reminder
expect(output.output).toContain("[Category+Skill Reminder]")
clearSessionAgent(sessionID)
})