Shortens a subagent's idle_notification when that agent already delivered its report by SendMessage or an earlier idle result.

A Claude Code mod that stops a subagent's completion from arriving twice. A subagent that reports to the main conversation by SendMessage is followed by the engine's idle_notification, whose result restates the same report; a late message can also wake a finished agent for one more turn and a second notice. This mod keeps such a repeat's name and idleReason and cuts its result to one line that begins with the first 120 characters of the final text.
A notice is cut only when all of these hold:
main, or already delivered an idle result, within the last 15 minutes;idleReason is absent or available, so errors are never cut.From the davesmods marketplace:
claude plugin marketplace add davethedev3/davesmods
claude plugin install idle-dedupe@davesmods
Function-hooks mods are an early-access Claude Code feature (written against 2.1.302). To run from a checkout instead, load the folder with claude --plugin-dir /path/to/idle-dedupe, or list it in CLAUDE_CODE_PLUGIN_DIRS (colon-separated) to load it in every session. The engine writes the plugin API's type declarations to .claude-plugin/types/ when it loads the mod; they are not part of this repo.
Checks: claude plugin validate . and claude plugin test . from this folder. The SendMessage path can't be raised from the test kit, so it was verified with a live subagent.
hooks/register.ts 74 lines1import type { Register } from 'claude-code'
2
3// A subagent's completion reaches the main conversation twice: once as the
4// report it sent by SendMessage, and again as the engine's idle_notification
5// carrying its final text, which restates the same report. A late inbox
6// message can also wake a finished agent for one more turn, which ends in a
7// second idle_notification. This hook keeps the name and idleReason of such a
8// repeat and cuts its result to one line.
9//
10// Measured 2026-10-04 on 2.1.289: a subagent's SendMessage to main raises
11// session.send with the sender's agentId and never reaches session.receive;
12// the idle_notification reaches session.receive as bare JSON text, the
13// <teammate-message> wrapper being added afterwards.
14
15const WINDOW_MS = 15 * 60 * 1000
16
17const reported = new Map<string, number>() // agent name -> time it sent a report to main
18const idled = new Map<string, number>() // agent name -> time its last idle result arrived
19const asked = new Map<string, number>() // agent name -> time main last messaged it
20
21const isRecent = (at: number | undefined, now: number) => at !== undefined && now - at <= WINDOW_MS
22
23// A notice repeats an earlier delivery only when nothing was asked of the
24// agent since then; a reply to a follow-up SendMessage is a new answer.
25function isRepeat(from: string, now: number): boolean {
26 const last = Math.max(reported.get(from) ?? 0, idled.get(from) ?? 0)
27 if (!isRecent(last, now)) return false
28 const askedAt = asked.get(from)
29 return askedAt === undefined || askedAt < last
30}
31
32function parseIdle(text: string): Record<string, unknown> | undefined {
33 const body = text.trim()
34 if (!body.startsWith('{')) return undefined
35 try {
36 const value = JSON.parse(body)
37 return value && value.type === 'idle_notification' && typeof value.from === 'string' ? value : undefined
38 } catch {
39 return undefined
40 }
41}
42
43export const register: Register = on => {
44 on('session.send', async ($, e, next) => {
45 if (e.agentId !== undefined && e.to === 'main') {
46 const sender = (await $.agent.list()).find(a => a.id === e.agentId)
47 if (sender?.name !== undefined) reported.set(sender.name, await $.clock.now())
48 } else if (e.agentId === undefined) {
49 const now = await $.clock.now()
50 asked.set(e.to, now)
51 const target = (await $.agent.list()).find(a => a.id === e.to || a.teammateId === e.to)
52 if (target?.name !== undefined) asked.set(target.name, now)
53 }
54 return next(e)
55 })
56
57 on('session.receive', async ($, e, next) => {
58 const notice = parseIdle(e.text)
59 if (notice === undefined) return next(e)
60
61 const now = await $.clock.now()
62 const from = notice.from as string
63 const result = typeof notice.result === 'string' ? notice.result : undefined
64 const reason = notice.idleReason
65 const cut = (reason === undefined || reason === 'available') && isRepeat(from, now) && result !== undefined
66 idled.set(from, now)
67 if (!cut || result === undefined) return next(e)
68
69 const head = result.replace(/\s+/g, ' ').slice(0, 120)
70 notice.result = `[idle-dedupe] ${from} is idle; its report was already delivered above. Final text began: ${head}`
71 return next({ ...e, text: JSON.stringify(notice) })
72 })
73}
74