-
Notifications
You must be signed in to change notification settings - Fork 712
fix(cli): suppress premature stop-hook notifications during auto-continue #2146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -328,6 +328,9 @@ private struct ClaudeHookSessionRecord: Codable { | |
| var lastBody: String? | ||
| var startedAt: TimeInterval | ||
| var updatedAt: TimeInterval | ||
| /// Timestamp of the most recent pre-tool-use event. Used to suppress | ||
| /// premature stop-hook notifications when Claude auto-continues mid-task. | ||
| var lastToolUseAt: TimeInterval? | ||
| } | ||
|
|
||
| private struct ClaudeHookSessionStoreFile: Codable { | ||
|
|
@@ -373,7 +376,8 @@ private final class ClaudeHookSessionStore { | |
| cwd: String?, | ||
| pid: Int? = nil, | ||
| lastSubtitle: String? = nil, | ||
| lastBody: String? = nil | ||
| lastBody: String? = nil, | ||
| lastToolUseAt: TimeInterval? = nil | ||
| ) throws { | ||
| let normalized = normalizeSessionId(sessionId) | ||
| guard !normalized.isEmpty else { return } | ||
|
|
@@ -406,6 +410,9 @@ private final class ClaudeHookSessionStore { | |
| if let body = normalizeOptional(lastBody) { | ||
| record.lastBody = body | ||
| } | ||
| if let lastToolUseAt { | ||
| record.lastToolUseAt = lastToolUseAt | ||
| } | ||
| record.updatedAt = now | ||
| state.sessions[normalized] = record | ||
| } | ||
|
|
@@ -10328,11 +10335,24 @@ struct CMUXCLI { | |
| } | ||
|
|
||
| if let completion { | ||
| let title = "Claude Code" | ||
| let subtitle = sanitizeNotificationField(completion.subtitle) | ||
| let body = sanitizeNotificationField(completion.body) | ||
| let payload = "\(title)|\(subtitle)|\(body)" | ||
| _ = try? sendV1Command("notify_target \(workspaceId) \(surfaceId) \(payload)", client: client) | ||
| // Suppress notification if a tool was used very recently — this | ||
| // indicates an auto-continue turn (Claude stopped briefly between | ||
| // tool calls) rather than true task completion. The threshold of | ||
| // 2 seconds is generous: auto-continue stop→pre-tool-use gaps are | ||
| // typically < 500 ms, while genuine completions require user think-time. | ||
| let shouldNotify: Bool | ||
| if let lastToolUse = mappedSession?.lastToolUseAt { | ||
| shouldNotify = Date().timeIntervalSince1970 - lastToolUse > 2.0 | ||
| } else { | ||
| shouldNotify = true | ||
| } | ||
| if shouldNotify { | ||
| let title = "Claude Code" | ||
| let subtitle = sanitizeNotificationField(completion.subtitle) | ||
| let body = sanitizeNotificationField(completion.body) | ||
| let payload = "\(title)|\(subtitle)|\(body)" | ||
| _ = try? sendV1Command("notify_target \(workspaceId) \(surfaceId) \(payload)", client: client) | ||
|
Comment on lines
+10350
to
+10354
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Localize the new completion notification title. This As per coding guidelines, 🤖 Prompt for AI Agents |
||
| } | ||
|
Comment on lines
10337
to
+10355
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The heuristic correctly handles the common case, but the 2-second threshold measures the gap between For fast tool calls (e.g. Concretely: The PR description notes "genuine completions require user think-time", which holds when Claude produces a long response, but not for short, fast final replies. Consider documenting this known limitation and/or providing a mechanism to tune the threshold via a user default (similar to |
||
| } | ||
|
|
||
| try? setClaudeStatus( | ||
|
|
@@ -10465,6 +10485,19 @@ struct CMUXCLI { | |
| ) | ||
| let claudePid = mappedSession?.pid | ||
|
|
||
| // Record the timestamp of this tool use so the stop handler can detect | ||
| // auto-continue turns (stop fired immediately after a tool use) and | ||
| // suppress premature notifications. | ||
| if let sessionId = parsedInput.sessionId, let existingSession = mappedSession { | ||
| try? sessionStore.upsert( | ||
| sessionId: sessionId, | ||
| workspaceId: workspaceId, | ||
| surfaceId: existingSession.surfaceId, | ||
| cwd: parsedInput.cwd, | ||
| lastToolUseAt: Date().timeIntervalSince1970 | ||
| ) | ||
| } | ||
|
Comment on lines
+10491
to
+10499
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Practical impact: A user who quickly follows up after a tool-heavy turn and gets a fast tool-free reply could silently miss the completion notification. Adding a // prompt-submit case — after clearing notifications
if let sessionId = parsedInput.sessionId, let existingSession = mappedSession {
try? sessionStore.upsert(
sessionId: sessionId,
workspaceId: workspaceId,
surfaceId: existingSession.surfaceId,
cwd: parsedInput.cwd
// lastToolUseAt not passed → remains nil after explicit reset
)
}This would require adding an explicit "clear" path (e.g. a |
||
|
|
||
| // AskUserQuestion means Claude is about to ask the user something. | ||
| // Save question text in session so the Notification handler can use it | ||
| // instead of the generic "Claude Code needs your attention". | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a session-wide cooldown, not a turn-local signal.
Line 10491 only sets
lastToolUseAt, and Line 10344 then treats it as a 2-second global cooldown. That means a laterstopin the same Claude session can suppress a real completion even when the current turn never used a tool, e.g. a quick no-tool follow-up right afterprompt-submit.Also applies to: 10488-10499
🤖 Prompt for AI Agents