@@ -114,6 +114,7 @@ public struct KiroStatusProbe: Sendable {
114114 let stdout : String
115115 let stderr : String
116116 let terminationStatus : Int32
117+ let terminatedForIdle : Bool
117118 }
118119
119120 private func ensureLoggedIn( ) async throws {
@@ -147,7 +148,10 @@ public struct KiroStatusProbe: Sendable {
147148 }
148149
149150 private func runUsageCommand( ) async throws -> String {
150- let result = try await self . runCommand ( arguments: [ " chat " , " --no-interactive " , " /usage " ] , timeout: 20.0 )
151+ let result = try await self . runCommand (
152+ arguments: [ " chat " , " --no-interactive " , " /usage " ] ,
153+ timeout: 20.0 ,
154+ idleTimeout: 10.0 )
151155 let trimmedStdout = result. stdout. trimmingCharacters ( in: . whitespacesAndNewlines)
152156 let trimmedStderr = result. stderr. trimmingCharacters ( in: . whitespacesAndNewlines)
153157 let combinedOutput = trimmedStderr. isEmpty ? trimmedStdout : trimmedStderr
@@ -162,6 +166,10 @@ public struct KiroStatusProbe: Sendable {
162166 throw KiroStatusProbeError . notLoggedIn
163167 }
164168
169+ if result. terminatedForIdle, !Self. isUsageOutputComplete ( combinedOutput) {
170+ throw KiroStatusProbeError . timeout
171+ }
172+
165173 if !trimmedStdout. isEmpty {
166174 return result. stdout
167175 }
@@ -183,8 +191,8 @@ public struct KiroStatusProbe: Sendable {
183191 private func runCommand(
184192 arguments: [ String ] ,
185193 timeout: TimeInterval ,
186- idleTimeout: TimeInterval = 5.0
187- ) async throws -> KiroCLIResult {
194+ idleTimeout: TimeInterval = 5.0 ) async throws -> KiroCLIResult
195+ {
188196 guard let binary = TTYCommandRunner . which ( " kiro-cli " ) else {
189197 throw KiroStatusProbeError . cliNotFound
190198 }
@@ -212,37 +220,37 @@ public struct KiroStatusProbe: Sendable {
212220 private var _stderrData = Data ( )
213221
214222 var lastActivityAt : Date {
215- lock. lock ( )
223+ self . lock. lock ( )
216224 defer { lock. unlock ( ) }
217- return _lastActivityAt
225+ return self . _lastActivityAt
218226 }
219227
220228 var hasReceivedOutput : Bool {
221- lock. lock ( )
229+ self . lock. lock ( )
222230 defer { lock. unlock ( ) }
223- return _hasReceivedOutput
231+ return self . _hasReceivedOutput
224232 }
225233
226234 func appendStdout( _ data: Data ) {
227- lock. lock ( )
235+ self . lock. lock ( )
228236 defer { lock. unlock ( ) }
229- _stdoutData. append ( data)
230- _lastActivityAt = Date ( )
231- _hasReceivedOutput = true
237+ self . _stdoutData. append ( data)
238+ self . _lastActivityAt = Date ( )
239+ self . _hasReceivedOutput = true
232240 }
233241
234242 func appendStderr( _ data: Data ) {
235- lock. lock ( )
243+ self . lock. lock ( )
236244 defer { lock. unlock ( ) }
237- _stderrData. append ( data)
238- _lastActivityAt = Date ( )
239- _hasReceivedOutput = true
245+ self . _stderrData. append ( data)
246+ self . _lastActivityAt = Date ( )
247+ self . _hasReceivedOutput = true
240248 }
241249
242250 func getOutput( ) -> ( stdout: Data , stderr: Data ) {
243- lock. lock ( )
251+ self . lock. lock ( )
244252 defer { lock. unlock ( ) }
245- return ( _stdoutData, _stderrData)
253+ return ( self . _stdoutData, self . _stderrData)
246254 }
247255 }
248256
@@ -274,13 +282,20 @@ public struct KiroStatusProbe: Sendable {
274282 }
275283
276284 let deadline = Date ( ) . addingTimeInterval ( timeout)
285+ var didHitDeadline = false
286+ var didTerminateForIdle = false
277287
278- while process. isRunning, Date ( ) < deadline {
288+ while process. isRunning {
289+ if Date ( ) >= deadline {
290+ didHitDeadline = true
291+ break
292+ }
279293 // Idle timeout: if we got output but then it went silent
280294 if state. hasReceivedOutput,
281295 Date ( ) . timeIntervalSince ( state. lastActivityAt) >= idleTimeout
282296 {
283297 // Process went idle after producing output - likely done or stuck
298+ didTerminateForIdle = true
284299 break
285300 }
286301 Thread . sleep ( forTimeInterval: 0.1 )
@@ -293,8 +308,7 @@ public struct KiroStatusProbe: Sendable {
293308 if process. isRunning {
294309 process. terminate ( )
295310 process. waitUntilExit ( )
296- // Only throw timeout if we never got output
297- if !state. hasReceivedOutput {
311+ if didHitDeadline || !state. hasReceivedOutput {
298312 continuation. resume ( throwing: KiroStatusProbeError . timeout)
299313 return
300314 }
@@ -313,7 +327,8 @@ public struct KiroStatusProbe: Sendable {
313327 continuation. resume ( returning: KiroCLIResult (
314328 stdout: stdoutOutput,
315329 stderr: stderrOutput,
316- terminationStatus: process. terminationStatus) )
330+ terminationStatus: process. terminationStatus,
331+ terminatedForIdle: didTerminateForIdle) )
317332 }
318333 }
319334 }
@@ -342,7 +357,6 @@ public struct KiroStatusProbe: Sendable {
342357 }
343358
344359 // Track which key patterns matched to detect format changes
345- var matchedPlanName = false
346360 var matchedPercent = false
347361 var matchedCredits = false
348362
@@ -351,7 +365,6 @@ public struct KiroStatusProbe: Sendable {
351365 if let planMatch = stripped. range ( of: #"\|\s*(KIRO\s+\w+)"# , options: . regularExpression) {
352366 let raw = String ( stripped [ planMatch] ) . replacingOccurrences ( of: " | " , with: " " )
353367 planName = raw. trimmingCharacters ( in: . whitespaces)
354- matchedPlanName = true
355368 }
356369
357370 // Parse reset date from "resets on 01/01"
@@ -411,7 +424,7 @@ public struct KiroStatusProbe: Sendable {
411424 }
412425
413426 // Require at least one key pattern to match to avoid silent failures
414- if !matchedPlanName , ! matchedPercent, !matchedCredits {
427+ if !matchedPercent, !matchedCredits {
415428 throw KiroStatusProbeError . parseError (
416429 " No recognizable usage patterns found. Kiro CLI output format may have changed. " )
417430 }
@@ -463,4 +476,11 @@ public struct KiroStatusProbe: Sendable {
463476 components. year = currentYear + 1
464477 return calendar. date ( from: components)
465478 }
479+
480+ private static func isUsageOutputComplete( _ output: String ) -> Bool {
481+ let stripped = self . stripANSI ( output) . lowercased ( )
482+ return stripped. contains ( " covered in plan " )
483+ || stripped. contains ( " resets on " )
484+ || stripped. contains ( " bonus credits " )
485+ }
466486}
0 commit comments