@@ -8,7 +8,7 @@ use crate::safety::{self, PermissionRequest, PermissionResult, SafetySystem, Urg
88use anyhow:: Result ;
99use async_trait:: async_trait;
1010use chrono:: Utc ;
11- use serde:: Deserialize ;
11+ use serde:: { Deserialize , Deserializer } ;
1212use serde_json:: { Map , Value , json} ;
1313use std:: collections:: HashSet ;
1414use std:: sync:: { Arc , Mutex , OnceLock } ;
@@ -119,10 +119,98 @@ impl EndAmbientCycleTool {
119119 }
120120}
121121
122+ // ---------------------------------------------------------------------------
123+ // Custom deserializers: accept either a JSON number or a numeric string for
124+ // u32 fields. Claude tool calls occasionally serialize numeric arguments as
125+ // strings (e.g. {"compactions": "0"} instead of {"compactions": 0}), which
126+ // caused every ambient cycle to fail with `invalid type: string "0", expected
127+ // u32`. See issue #133 / upstream PR #173.
128+ // ---------------------------------------------------------------------------
129+
130+ fn deserialize_string_or_u32 < ' de , D > ( deserializer : D ) -> Result < u32 , D :: Error >
131+ where
132+ D : Deserializer < ' de > ,
133+ {
134+ use serde:: de:: { self , Visitor } ;
135+ struct StringOrU32 ;
136+
137+ impl < ' de > Visitor < ' de > for StringOrU32 {
138+ type Value = u32 ;
139+
140+ fn expecting ( & self , f : & mut std:: fmt:: Formatter ) -> std:: fmt:: Result {
141+ f. write_str ( "u32 or string representing u32" )
142+ }
143+
144+ fn visit_u64 < E > ( self , v : u64 ) -> Result < Self :: Value , E >
145+ where
146+ E : de:: Error ,
147+ {
148+ u32:: try_from ( v) . map_err ( E :: custom)
149+ }
150+
151+ fn visit_str < E > ( self , v : & str ) -> Result < Self :: Value , E >
152+ where
153+ E : de:: Error ,
154+ {
155+ v. parse ( ) . map_err ( E :: custom)
156+ }
157+ }
158+
159+ deserializer. deserialize_any ( StringOrU32 )
160+ }
161+
162+ fn deserialize_string_or_option_u32 < ' de , D > ( deserializer : D ) -> Result < Option < u32 > , D :: Error >
163+ where
164+ D : Deserializer < ' de > ,
165+ {
166+ use serde:: de:: { self , Visitor } ;
167+ struct StringOrOptionU32 ;
168+
169+ impl < ' de > Visitor < ' de > for StringOrOptionU32 {
170+ type Value = Option < u32 > ;
171+
172+ fn expecting ( & self , f : & mut std:: fmt:: Formatter ) -> std:: fmt:: Result {
173+ f. write_str ( "optional u32 or string representing u32" )
174+ }
175+
176+ fn visit_none < E > ( self ) -> Result < Self :: Value , E >
177+ where
178+ E : de:: Error ,
179+ {
180+ Ok ( None )
181+ }
182+
183+ fn visit_some < D > ( self , deserializer : D ) -> Result < Self :: Value , D :: Error >
184+ where
185+ D : Deserializer < ' de > ,
186+ {
187+ deserialize_string_or_u32 ( deserializer) . map ( Some )
188+ }
189+
190+ fn visit_u64 < E > ( self , v : u64 ) -> Result < Self :: Value , E >
191+ where
192+ E : de:: Error ,
193+ {
194+ u32:: try_from ( v) . map_err ( E :: custom) . map ( Some )
195+ }
196+
197+ fn visit_str < E > ( self , v : & str ) -> Result < Self :: Value , E >
198+ where
199+ E : de:: Error ,
200+ {
201+ v. parse ( ) . map_err ( E :: custom) . map ( Some )
202+ }
203+ }
204+
205+ deserializer. deserialize_option ( StringOrOptionU32 )
206+ }
207+
122208#[ derive( Deserialize ) ]
123209struct EndCycleInput {
124210 summary : String ,
211+ #[ serde( deserialize_with = "deserialize_string_or_u32" ) ]
125212 memories_modified : u32 ,
213+ #[ serde( deserialize_with = "deserialize_string_or_u32" ) ]
126214 compactions : u32 ,
127215 #[ serde( default ) ]
128216 proactive_work : Option < String > ,
@@ -132,7 +220,7 @@ struct EndCycleInput {
132220
133221#[ derive( Deserialize ) ]
134222struct NextScheduleInput {
135- #[ serde( default ) ]
223+ #[ serde( default , deserialize_with = "deserialize_string_or_option_u32" ) ]
136224 wake_in_minutes : Option < u32 > ,
137225 #[ serde( default ) ]
138226 context : Option < String > ,
@@ -280,7 +368,7 @@ impl ScheduleAmbientTool {
280368
281369#[ derive( Deserialize ) ]
282370struct ScheduleInput {
283- #[ serde( default ) ]
371+ #[ serde( default , deserialize_with = "deserialize_string_or_option_u32" ) ]
284372 wake_in_minutes : Option < u32 > ,
285373 #[ serde( default ) ]
286374 wake_at : Option < String > ,
@@ -722,7 +810,7 @@ struct ScheduleToolInput {
722810 schedule_id : Option < String > ,
723811 #[ serde( default ) ]
724812 task : Option < String > ,
725- #[ serde( default ) ]
813+ #[ serde( default , deserialize_with = "deserialize_string_or_option_u32" ) ]
726814 wake_in_minutes : Option < u32 > ,
727815 #[ serde( default ) ]
728816 wake_at : Option < String > ,
0 commit comments