1010import com .pyritone .bridge .net .SocketBridgeServer ;
1111import com .pyritone .bridge .runtime .BaritoneGateway ;
1212import com .pyritone .bridge .runtime .TaskRegistry ;
13+ import com .pyritone .bridge .runtime .TaskLifecycleResolver ;
1314import com .pyritone .bridge .runtime .TaskSnapshot ;
1415import com .pyritone .bridge .runtime .TaskState ;
15- import com .pyritone .bridge .runtime .TaskLifecycleResolver ;
1616import com .pyritone .bridge .runtime .WatchPatternRegistry ;
1717import net .fabricmc .api .ClientModInitializer ;
1818import net .fabricmc .fabric .api .client .event .lifecycle .v1 .ClientTickEvents ;
@@ -64,6 +64,7 @@ public void onInitializeClient() {
6464
6565 ClientTickEvents .END_CLIENT_TICK .register (client -> {
6666 baritoneGateway .tickRegisterPathListener ();
67+ baritoneGateway .tickRegisterPyritoneHashCommand (this ::forceCancelActiveTaskFromPyritoneCommand );
6768 baritoneGateway .tickApplyPyritoneChatBranding ();
6869 tickTaskLifecycle ();
6970 });
@@ -118,6 +119,10 @@ private void ensureBaritoneSettingsFile() {
118119 }
119120 }
120121 private boolean handleOutgoingChat (String message ) {
122+ if (message != null && message .trim ().equalsIgnoreCase ("#pyritone cancel" )) {
123+ forceCancelActiveTaskFromPyritoneCommand ();
124+ return false ;
125+ }
121126 emitWatchMatches ("chat" , message );
122127 return true ;
123128 }
@@ -333,6 +338,35 @@ private JsonObject handleTaskCancel(String id) {
333338 return ProtocolCodec .successResponse (id , result );
334339 }
335340
341+ public boolean forceCancelActiveTaskFromPyritoneCommand () {
342+ Optional <TaskSnapshot > active = taskRegistry .active ();
343+ if (active .isEmpty ()) {
344+ emitPyritoneNotice ("No active Py-Ritone task to cancel" );
345+ return false ;
346+ }
347+
348+ TaskSnapshot snapshot = active .orElseThrow ();
349+ String detail = "Canceled by #pyritone cancel" ;
350+
351+ if (!baritoneGateway .isAvailable ()) {
352+ detail = "Canceled by #pyritone cancel (Baritone unavailable)" ;
353+ emitPyritoneNotice ("Hard cancel: Baritone unavailable, force-ending tracked task" );
354+ } else {
355+ BaritoneGateway .Outcome outcome = baritoneGateway .cancelCurrent ();
356+ if (outcome .ok ()) {
357+ emitPyritoneNotice ("Hard cancel accepted" );
358+ } else {
359+ detail = "Canceled by #pyritone cancel (" + compactCommand (outcome .message ()) + ")" ;
360+ emitPyritoneNotice ("Hard cancel forced task end even though Baritone cancel returned an error" );
361+ }
362+ }
363+
364+ taskLifecycleResolver .clearForTask (snapshot .taskId ());
365+ taskRegistry .transitionActive (TaskState .CANCELED , detail )
366+ .ifPresent (canceled -> emitTaskEvent ("task.canceled" , canceled , "pyritone_cancel_command" ));
367+ return true ;
368+ }
369+
336370 private void onPathEvent (String pathEventName ) {
337371 JsonObject data = new JsonObject ();
338372 data .addProperty ("path_event" , pathEventName );
@@ -348,8 +382,6 @@ private void onPathEvent(String pathEventName) {
348382
349383 TaskSnapshot current = active .orElseThrow ();
350384 taskLifecycleResolver .recordPathEvent (current .taskId (), pathEventName );
351- taskRegistry .updateActiveDetail (pathEventDetail (pathEventName ))
352- .ifPresent (snapshot -> emitTaskEvent ("task.progress" , snapshot , pathEventName ));
353385 }
354386
355387 private void tickTaskLifecycle () {
@@ -360,27 +392,41 @@ private void tickTaskLifecycle() {
360392 }
361393
362394 TaskSnapshot current = active .orElseThrow ();
363- Optional <TaskLifecycleResolver .TerminalDecision > terminal = taskLifecycleResolver .evaluate (
395+ Optional <TaskLifecycleResolver .LifecycleUpdate > lifecycleUpdate = taskLifecycleResolver .evaluate (
364396 current .taskId (),
365397 baritoneGateway .activitySnapshot ()
366398 );
367399
368- if (terminal .isEmpty ()) {
400+ if (lifecycleUpdate .isEmpty ()) {
369401 return ;
370402 }
371403
372- TaskLifecycleResolver .TerminalDecision decision = terminal .orElseThrow ();
373- taskRegistry .transitionActive (decision .state (), decision .detail ())
374- .ifPresent (snapshot -> emitTaskEvent (decision .eventName (), snapshot , decision .stage ()));
404+ TaskLifecycleResolver .LifecycleUpdate update = lifecycleUpdate .orElseThrow ();
405+ switch (update .kind ()) {
406+ case PAUSED -> handlePausedUpdate (current , update .pauseStatus ());
407+ case RESUMED -> handleResumedUpdate (current , update .pauseStatus ());
408+ case TERMINAL -> {
409+ TaskLifecycleResolver .TerminalDecision decision = update .terminalDecision ();
410+ if (decision == null ) {
411+ return ;
412+ }
413+ taskRegistry .transitionActive (decision .state (), decision .detail ())
414+ .ifPresent (snapshot -> emitTaskEvent (decision .eventName (), snapshot , decision .stage ()));
415+ }
416+ }
375417 }
376418
377- private static String pathEventDetail (String pathEventName ) {
378- return switch (pathEventName ) {
379- case "AT_GOAL" -> "Reached goal (awaiting stable idle)" ;
380- case "CANCELED" -> "Baritone canceled (awaiting stable idle)" ;
381- case "CALC_FAILED" , "NEXT_CALC_FAILED" -> "Path calculation failed (awaiting stable idle)" ;
382- default -> "Path event: " + pathEventName ;
383- };
419+ private void handlePausedUpdate (TaskSnapshot current , TaskLifecycleResolver .PauseStatus pauseStatus ) {
420+ String detail = pauseStatusDetail (pauseStatus );
421+ Optional <TaskSnapshot > updated = taskRegistry .updateActiveDetail (detail );
422+ TaskSnapshot snapshot = updated .orElse (current );
423+ emitPauseEvent ("task.paused" , snapshot , pauseStatus );
424+ }
425+
426+ private void handleResumedUpdate (TaskSnapshot current , TaskLifecycleResolver .PauseStatus pauseStatus ) {
427+ Optional <TaskSnapshot > updated = taskRegistry .updateActiveDetail ("Resumed after pause" );
428+ TaskSnapshot snapshot = updated .orElse (current );
429+ emitPauseEvent ("task.resumed" , snapshot , pauseStatus );
384430 }
385431
386432 private void emitTaskEvent (String eventName , TaskSnapshot taskSnapshot , String stage ) {
@@ -391,6 +437,40 @@ private void emitTaskEvent(String eventName, TaskSnapshot taskSnapshot, String s
391437 publishEvent (eventName , data );
392438 }
393439
440+ private void emitPauseEvent (String eventName , TaskSnapshot taskSnapshot , TaskLifecycleResolver .PauseStatus pauseStatus ) {
441+ JsonObject data = taskSnapshot .toJson ();
442+ data .addProperty ("stage" , eventName );
443+ data .add ("pause" , pauseStatusToJson (pauseStatus ));
444+ publishEvent (eventName , data );
445+ }
446+
447+ private static JsonObject pauseStatusToJson (TaskLifecycleResolver .PauseStatus pauseStatus ) {
448+ JsonObject object = new JsonObject ();
449+ if (pauseStatus == null ) {
450+ object .addProperty ("reason_code" , "PAUSED" );
451+ object .addProperty ("source_process" , "" );
452+ object .addProperty ("command_type" , "" );
453+ return object ;
454+ }
455+
456+ object .addProperty ("reason_code" , safeString (pauseStatus .reasonCode ()));
457+ object .addProperty ("source_process" , safeString (pauseStatus .sourceProcess ()));
458+ object .addProperty ("command_type" , safeString (pauseStatus .commandType ()));
459+ return object ;
460+ }
461+
462+ private static String pauseStatusDetail (TaskLifecycleResolver .PauseStatus pauseStatus ) {
463+ if (pauseStatus == null ) {
464+ return "Paused" ;
465+ }
466+
467+ return "Paused (" + safeString (pauseStatus .reasonCode ()) + ")" ;
468+ }
469+
470+ private static String safeString (String value ) {
471+ return value == null ? "" : value ;
472+ }
473+
394474 private void publishEvent (String eventName , JsonObject data ) {
395475 SocketBridgeServer currentServer = this .server ;
396476 if (currentServer == null || !currentServer .isRunning ()) {
0 commit comments