fix(hooks): store a hook delivered twice only once

A doubled hook registration (plugin + leftover checkout install) POSTs every
event twice with a byte-identical payload, so the timeline showed each row
twice and token/alert/lane side effects ran twice. POST /api/hooks/event now
skips a payload identical to the session's LATEST event from the last 10s
(findRecentDuplicateEvent). Latest-only keeps two legitimately identical
Stop events of consecutive turns, which always have other events between.
This commit is contained in:
2026-09-23 09:18:17 +07:00
parent 1b561c22d9
commit 92cee94431
6 changed files with 66 additions and 8 deletions
+3 -3
View File
@@ -216,13 +216,13 @@ describe("Event-driven alert evaluation", () => {
const ruleId = created.body.rule.id;
const sessionId = `alerts-burst-${Date.now()}`;
await postHook("BurstProbe", { session_id: sessionId });
await postHook("BurstProbe", { session_id: sessionId });
await postHook("BurstProbe", { session_id: sessionId, seq: 1 });
await postHook("BurstProbe", { session_id: sessionId, seq: 2 });
let feed = await fetch("/api/alerts");
assert.equal(feed.body.alerts.filter((a) => a.rule_id === ruleId).length, 0);
await postHook("BurstProbe", { session_id: sessionId });
await postHook("BurstProbe", { session_id: sessionId, seq: 3 });
feed = await fetch("/api/alerts");
const fired = feed.body.alerts.filter((a) => a.rule_id === ruleId);
assert.equal(fired.length, 1);
+31
View File
@@ -617,6 +617,37 @@ describe("Hook Event Processing", () => {
assert.equal(res.body.error.code, "MISSING_SESSION");
});
it("stores a hook delivered twice only once (double registration)", async () => {
const hook = {
hook_type: "PreToolUse",
data: {
session_id: "hook-dup-1",
tool_name: "Bash",
tool_use_id: "toolu_dup_1",
tool_input: { command: "ls", description: "List files" },
},
};
const first = await post("/api/hooks/event", hook);
const second = await post("/api/hooks/event", hook);
assert.equal(first.status, 200);
assert.equal(second.status, 200);
assert.equal(second.body.duplicate, true);
const { n } = db
.prepare("SELECT COUNT(*) AS n FROM events WHERE session_id = 'hook-dup-1'")
.get();
assert.equal(n, 1);
// A DIFFERENT payload in the same session still lands.
await post("/api/hooks/event", {
...hook,
data: { ...hook.data, tool_use_id: "toolu_dup_2" },
});
const after = db
.prepare("SELECT COUNT(*) AS n FROM events WHERE session_id = 'hook-dup-1'")
.get();
assert.equal(after.n, 2);
});
it("should auto-create session and main agent on first PreToolUse", async () => {
const res = await post("/api/hooks/event", {
hook_type: "PreToolUse",
+2 -2
View File
@@ -172,7 +172,7 @@ describe("hook ingestor — sessions.name sync from transcript", () => {
]);
await req("POST", "/api/hooks/event", {
hook_type: "Stop",
data: { session_id: sid, cwd, transcript_path: tpath },
data: { session_id: sid, cwd, transcript_path: tpath, turn: 1 },
});
assert.equal(stmts.getSession.get(sid).name, "keep-me");
@@ -184,7 +184,7 @@ describe("hook ingestor — sessions.name sync from transcript", () => {
]);
await req("POST", "/api/hooks/event", {
hook_type: "Stop",
data: { session_id: sid, cwd, transcript_path: tpath },
data: { session_id: sid, cwd, transcript_path: tpath, turn: 2 },
});
assert.equal(stmts.getSession.get(sid).name, "renamed-explicitly");
});