@@ -299,7 +299,10 @@ public CompletableFuture<KimiAcpTurnResult> promptAsync(String sessionId, String
299299 throw new IllegalStateException ("kimi acp client is closed" );
300300 }
301301 PromptStream stream = new PromptStream (sessionId , onDelta );
302- promptStreams .put (sessionId , stream );
302+ PromptStream active = promptStreams .putIfAbsent (sessionId , stream );
303+ if (active != null ) {
304+ throw new KimiException ("kimi acp session already has an active prompt: " + sessionId );
305+ }
303306 Map <String , Object > content = new LinkedHashMap <String , Object >();
304307 content .put ("type" , "text" );
305308 content .put ("text" , text );
@@ -308,14 +311,24 @@ public CompletableFuture<KimiAcpTurnResult> promptAsync(String sessionId, String
308311 Map <String , Object > params = new LinkedHashMap <String , Object >();
309312 params .put ("sessionId" , sessionId );
310313 params .put ("prompt" , blocks );
311- CompletableFuture <JsonNode > response = request ("session/prompt" , params );
314+ final CompletableFuture <JsonNode > response ;
315+ try {
316+ response = request ("session/prompt" , params );
317+ } catch (RuntimeException e ) {
318+ promptStreams .remove (sessionId , stream );
319+ throw e ;
320+ }
312321 CompletableFuture <KimiAcpTurnResult > future = response .thenApply (node -> {
322+ RuntimeException callbackFailure = stream .callbackFailure ();
323+ if (callbackFailure != null ) {
324+ throw new KimiException ("kimi acp prompt callback failed" , callbackFailure );
325+ }
313326 String stopReason = firstText (node , "stopReason" , "stop_reason" );
314327 return new KimiAcpTurnResult (sessionId , stopReason , stream .content ());
315328 });
316329 scheduleTimeout (future , config .getReadTimeoutMillis (), "session/prompt turn" );
317330 future .whenComplete ((r , error ) -> {
318- promptStreams .remove (sessionId );
331+ promptStreams .remove (sessionId , stream );
319332 if (error != null ) {
320333 response .completeExceptionally (error );
321334 }
@@ -427,8 +440,15 @@ public void close() {
427440 if (!closed .compareAndSet (false , true )) {
428441 return ;
429442 }
443+ connected .set (false );
430444 timer .shutdownNow ();
445+ PrintWriter writer = stdin ;
446+ stdin = null ;
447+ if (writer != null ) {
448+ writer .close ();
449+ }
431450 Process current = process ;
451+ process = null ;
432452 if (current != null ) {
433453 current .destroy ();
434454 }
@@ -447,8 +467,16 @@ private CompletableFuture<JsonNode> request(String method, Map<String, Object> p
447467 payload .put ("method" , method );
448468 payload .put ("params" , params );
449469 CompletableFuture <JsonNode > future = new CompletableFuture <JsonNode >();
450- pendingRpcs .put (Long .valueOf (id ), future );
451- writeJson (payload );
470+ Long rpcId = Long .valueOf (id );
471+ pendingRpcs .put (rpcId , future );
472+ future .whenComplete ((result , error ) -> pendingRpcs .remove (rpcId , future ));
473+ try {
474+ writeJson (payload );
475+ } catch (RuntimeException e ) {
476+ pendingRpcs .remove (rpcId , future );
477+ future .completeExceptionally (e );
478+ throw e ;
479+ }
452480 return future ;
453481 }
454482
@@ -508,6 +536,16 @@ private void readLoop() {
508536 if (!closed .get ()) {
509537 failAllPending (new KimiException ("kimi acp stdout read failed" , e ));
510538 }
539+ } catch (KimiException e ) {
540+ if (!closed .get ()) {
541+ failAllPending (e );
542+ Process current = process ;
543+ if (current != null ) {
544+ current .destroy ();
545+ }
546+ }
547+ } finally {
548+ connected .set (false );
511549 }
512550 }
513551
@@ -516,8 +554,7 @@ private void handleFrame(String frame) {
516554 try {
517555 node = mapper .readTree (frame );
518556 } catch (Exception ex ) {
519- log .warn ("Ignored non-JSON frame from kimi acp" );
520- return ;
557+ throw new KimiException ("kimi acp protocol received invalid JSON frame" , ex );
521558 }
522559 if (node .hasNonNull ("id" )) {
523560 CompletableFuture <JsonNode > pending = pendingRpcs .remove (Long .valueOf (node .get ("id" ).asLong ()));
@@ -631,6 +668,7 @@ private final class PromptStream {
631668 private final StringBuilder content = new StringBuilder ();
632669 private final Consumer <String > onDelta ;
633670 private boolean truncationWarned ;
671+ private volatile RuntimeException callbackFailure ;
634672
635673 PromptStream (String sessionId , Consumer <String > onDelta ) {
636674 this .onDelta = onDelta ;
@@ -653,11 +691,20 @@ void append(String text) {
653691 }
654692 }
655693 content .append (applied );
656- if (!applied .isEmpty () && onDelta != null ) {
657- onDelta .accept (applied );
694+ if (!applied .isEmpty () && onDelta != null && callbackFailure == null ) {
695+ try {
696+ onDelta .accept (applied );
697+ } catch (RuntimeException e ) {
698+ callbackFailure = e ;
699+ log .warn ("kimi acp prompt callback failed; transport remains active" );
700+ }
658701 }
659702 }
660703
704+ RuntimeException callbackFailure () {
705+ return callbackFailure ;
706+ }
707+
661708 String content () {
662709 return content .toString ();
663710 }
0 commit comments