@@ -3,8 +3,11 @@ import { z } from 'zod/v4'
33import type { ToolResultBlockParam } from 'src/Tool.js'
44import { buildTool } from 'src/Tool.js'
55import { lazySchema } from 'src/utils/lazySchema.js'
6+ import { notifyAutomationStateChanged } from 'src/utils/sessionState.js'
67import { SLEEP_TOOL_NAME , DESCRIPTION , SLEEP_TOOL_PROMPT } from './prompt.js'
78
9+ const SLEEP_WAKE_CHECK_INTERVAL_MS = 500
10+
811const inputSchema = lazySchema ( ( ) =>
912 z . strictObject ( {
1013 duration_seconds : z
@@ -19,6 +22,36 @@ type SleepInput = z.infer<InputSchema>
1922
2023type SleepOutput = { slept_seconds : number ; interrupted : boolean }
2124
25+ function isProactiveAutomationEnabled ( ) : boolean {
26+ if ( ! ( feature ( 'PROACTIVE' ) || feature ( 'KAIROS' ) ) ) {
27+ return false
28+ }
29+
30+ const mod =
31+ require ( 'src/proactive/index.js' ) as typeof import ( 'src/proactive/index.js' )
32+ return mod . isProactiveActive ( )
33+ }
34+
35+ function isProactiveSleepAllowed ( ) : boolean {
36+ if ( ! ( feature ( 'PROACTIVE' ) || feature ( 'KAIROS' ) ) ) {
37+ return true
38+ }
39+
40+ const mod =
41+ require ( 'src/proactive/index.js' ) as typeof import ( 'src/proactive/index.js' )
42+ return mod . isProactiveActive ( )
43+ }
44+
45+ function hasQueuedWakeSignal ( ) : boolean {
46+ const queue =
47+ require ( 'src/utils/messageQueueManager.js' ) as typeof import ( 'src/utils/messageQueueManager.js' )
48+ return queue . hasCommandsInQueue ( )
49+ }
50+
51+ function shouldInterruptSleep ( ) : boolean {
52+ return ! isProactiveSleepAllowed ( ) || hasQueuedWakeSignal ( )
53+ }
54+
2255export const SleepTool = buildTool ( {
2356 name : SLEEP_TOOL_NAME ,
2457 searchHint : 'wait pause sleep rest idle duration timer' ,
@@ -42,6 +75,9 @@ export const SleepTool = buildTool({
4275 isReadOnly ( ) {
4376 return true
4477 } ,
78+ interruptBehavior ( ) {
79+ return 'cancel'
80+ } ,
4581
4682 userFacingName ( ) {
4783 return SLEEP_TOOL_NAME
@@ -67,53 +103,84 @@ export const SleepTool = buildTool({
67103 } ,
68104
69105 async call ( input : SleepInput , context ) {
70- // Refuse to sleep when proactive mode is off — prevents the model from
71- // re-issuing Sleep after an interruption caused by /proactive disable.
72- if ( feature ( 'PROACTIVE' ) || feature ( 'KAIROS' ) ) {
73- const mod =
74- require ( 'src/proactive/index.js' ) as typeof import ( 'src/proactive/index.js' )
75- if ( ! mod . isProactiveActive ( ) ) {
76- return {
77- data : {
78- slept_seconds : 0 ,
79- interrupted : true ,
80- } ,
81- }
106+ // Don't enter sleep if proactive was disabled or new work arrived while
107+ // the model was deciding to wait.
108+ if ( shouldInterruptSleep ( ) ) {
109+ return {
110+ data : {
111+ slept_seconds : 0 ,
112+ interrupted : true ,
113+ } ,
82114 }
83115 }
84116
85117 const { duration_seconds } = input
86118 const startTime = Date . now ( )
119+ const sleepUntil = startTime + duration_seconds * 1000
120+
121+ if ( isProactiveAutomationEnabled ( ) ) {
122+ notifyAutomationStateChanged ( {
123+ enabled : true ,
124+ phase : 'sleeping' ,
125+ next_tick_at : null ,
126+ sleep_until : sleepUntil ,
127+ } )
128+ }
87129
88130 try {
89131 await new Promise < void > ( ( resolve , reject ) => {
90- const timer = setTimeout ( resolve , duration_seconds * 1000 )
132+ let timer : ReturnType < typeof setTimeout > | null = null
133+ let wakeCheck : ReturnType < typeof setInterval > | null = null
134+ let settled = false
91135
92- // Abort via user interrupt
93- context . abortController . signal . addEventListener (
94- 'abort' ,
95- ( ) => {
136+ const cleanup = ( ) => {
137+ if ( timer !== null ) {
96138 clearTimeout ( timer )
97- clearInterval ( proactiveCheck )
98- reject ( new Error ( 'interrupted' ) )
99- } ,
100- { once : true } ,
101- )
102-
103- // Poll proactive state — if deactivated mid-sleep, interrupt early
104- // so the user doesn't have to wait for the full duration.
105- const proactiveCheck =
106- feature ( 'PROACTIVE' ) || feature ( 'KAIROS' )
107- ? setInterval ( ( ) => {
108- const mod =
109- require ( 'src/proactive/index.js' ) as typeof import ( 'src/proactive/index.js' )
110- if ( ! mod . isProactiveActive ( ) ) {
111- clearTimeout ( timer )
112- clearInterval ( proactiveCheck )
113- reject ( new Error ( 'interrupted' ) )
114- }
115- } , 500 )
116- : ( null as unknown as ReturnType < typeof setInterval > )
139+ timer = null
140+ }
141+ if ( wakeCheck !== null ) {
142+ clearInterval ( wakeCheck )
143+ wakeCheck = null
144+ }
145+ context . abortController . signal . removeEventListener ( 'abort' , onAbort )
146+ }
147+
148+ const finish = ( ) => {
149+ if ( settled ) return
150+ settled = true
151+ cleanup ( )
152+ resolve ( )
153+ }
154+
155+ const interrupt = ( ) => {
156+ if ( settled ) return
157+ settled = true
158+ cleanup ( )
159+ reject ( new Error ( 'interrupted' ) )
160+ }
161+
162+ const onAbort = ( ) => {
163+ interrupt ( )
164+ }
165+
166+ timer = setTimeout ( finish , duration_seconds * 1000 )
167+
168+ // Abort via user interrupt
169+ if ( context . abortController . signal . aborted ) {
170+ interrupt ( )
171+ return
172+ }
173+ context . abortController . signal . addEventListener ( 'abort' , onAbort , {
174+ once : true ,
175+ } )
176+
177+ // Poll proactive state and the shared command queue so new work can
178+ // wake Sleep without waiting for the full duration.
179+ wakeCheck = setInterval ( ( ) => {
180+ if ( shouldInterruptSleep ( ) ) {
181+ interrupt ( )
182+ }
183+ } , SLEEP_WAKE_CHECK_INTERVAL_MS )
117184 } )
118185 return {
119186 data : {
@@ -129,6 +196,17 @@ export const SleepTool = buildTool({
129196 interrupted : true ,
130197 } ,
131198 }
199+ } finally {
200+ notifyAutomationStateChanged (
201+ isProactiveAutomationEnabled ( )
202+ ? {
203+ enabled : true ,
204+ phase : null ,
205+ next_tick_at : null ,
206+ sleep_until : null ,
207+ }
208+ : null ,
209+ )
132210 }
133211 } ,
134212} )
0 commit comments