From 175f4b35c5e7ed77e8362078b22ceec30b653e5f Mon Sep 17 00:00:00 2001 From: onevcat Date: Sat, 29 Aug 2026 00:17:48 +0900 Subject: [PATCH] Ignore Claude idle_prompt notifications in the managed hook decoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code fires a `Notification` of type `idle_prompt` 60 s after every turn while the composer sits empty. `decodeClaude` accepted it as `needs-input`, so on a Profile-launched pane it replaced `turn-ended` as the active terminal level: `agents wait --until idle` under `auto` then timed out (the level was a `needs-input` the detector never corroborates, and the `verified_live` channel suppressed the detector fallback), `--until changed` armed on an idle pane woke exactly 60 s later with no turn, and `--until blocked` would have resolved with exact confidence although nothing was blocked. The S3b decoder had already excluded `idle_prompt` for Copilot, Droid, and Qoder on the same reasoning — waiting for a person is not needing one — so both paths now share one `acceptedAttentionNotifications` list (`permission_prompt`, `elicitation_dialog`). Claude-Session: https://claude.ai/code/session_01J6ByKA2vhZCUebkxdTMbMr --- .../Shared/AgentNativeHookPayload.swift | 21 ++++++++---------- .../AgentNativeHookPayloadTests.swift | 22 +++++++++++++++---- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/supacode/CLIService/Shared/AgentNativeHookPayload.swift b/supacode/CLIService/Shared/AgentNativeHookPayload.swift index 484ab5e0..599016a1 100644 --- a/supacode/CLIService/Shared/AgentNativeHookPayload.swift +++ b/supacode/CLIService/Shared/AgentNativeHookPayload.swift @@ -80,16 +80,13 @@ nonisolated public enum AgentNativeHookDecodeError: Error, Equatable, Sendable { nonisolated public enum AgentNativeHookDecoder { public static let maximumPayloadBytes = 1_024 * 1_024 private static let maximumCWDBytes = 4 * 1_024 - private static let acceptedClaudeNotifications: Set = [ - "elicitation_dialog", - "idle_prompt", - "permission_prompt", - ] - - /// S3b runtimes report attention through `Notification` only. `idle_prompt` means the agent - /// is waiting rather than blocked on a person, and `auth_success` / background-task types are - /// informational, so neither may resolve a wait as `needs-input` (docs-ai 064.008). - private static let acceptedBlockingNotifications: Set = [ + /// `Notification` types that mean a person must act, for Claude Code and the S3b runtimes + /// alike. `idle_prompt` means the agent is waiting rather than blocked on a person — Claude + /// fires it 60 s after every turn at an empty composer, where it would displace the + /// `turn-ended` level idle waits read and wake `changed` waits — and `auth_success` / + /// background-task types are informational, so none may resolve a wait as `needs-input` + /// (docs-ai 064.008, 064.013). + private static let acceptedAttentionNotifications: Set = [ "elicitation_dialog", "permission_prompt", ] @@ -190,7 +187,7 @@ nonisolated public enum AgentNativeHookDecoder { let detail: String? if nativeEvent == "Notification" { guard let type = object["notification_type"] as? String, - acceptedBlockingNotifications.contains(type) + acceptedAttentionNotifications.contains(type) else { throw AgentNativeHookDecodeError.unsupportedEvent } @@ -233,7 +230,7 @@ nonisolated public enum AgentNativeHookDecoder { detail = boundedOptionalString(object["tool_name"] ?? object["reason"]) case "Notification": guard let type = object["notification_type"] as? String, - acceptedClaudeNotifications.contains(type) + acceptedAttentionNotifications.contains(type) else { throw AgentNativeHookDecodeError.unsupportedEvent } diff --git a/supacodeTests/AgentNativeHookPayloadTests.swift b/supacodeTests/AgentNativeHookPayloadTests.swift index 30e9cb61..0a925238 100644 --- a/supacodeTests/AgentNativeHookPayloadTests.swift +++ b/supacodeTests/AgentNativeHookPayloadTests.swift @@ -55,12 +55,26 @@ struct AgentNativeHookPayloadTests { #expect(signal.event == .needsInput) #expect(signal.detail == "permission_prompt") - let ignored = Data( - #"{"hook_event_name":"Notification","notification_type":"future_notice","session_id":"s","cwd":"/tmp/p"}"# + let elicitation = Data( + #"{"hook_event_name":"Notification","notification_type":"elicitation_dialog","session_id":"s","cwd":"/tmp/p"}"# .utf8 ) - #expect(throws: AgentNativeHookDecodeError.unsupportedEvent) { - try AgentNativeHookDecoder.decode(runtime: .claude, nativeEvent: "Notification", payload: ignored) + #expect( + try AgentNativeHookDecoder.decode(runtime: .claude, nativeEvent: "Notification", payload: elicitation).detail + == "elicitation_dialog" + ) + + // `idle_prompt` fires 60 s after a turn ended while the composer sits empty: the agent is + // waiting, not blocked on a person, and treating it as `needs-input` would displace the + // `turn-ended` level that idle waits rely on. + for rejected in ["idle_prompt", "auth_success", "future_notice"] { + let ignored = Data( + #"{"hook_event_name":"Notification","notification_type":"\#(rejected)","session_id":"s","cwd":"/tmp/p"}"# + .utf8 + ) + #expect(throws: AgentNativeHookDecodeError.unsupportedEvent) { + try AgentNativeHookDecoder.decode(runtime: .claude, nativeEvent: "Notification", payload: ignored) + } } } -- 2.51.2