Class: Rubino::Interaction::CancelToken

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/interaction/cancel_token.rb

Overview

Thread-safe cooperative cancellation flag passed through the interaction stack (Runner -> Lifecycle -> Loop -> LLM adapter). The chat TUI flips it on Esc / second Ctrl+C, and the LLM stream callback raises Rubino::Interrupted at the next chunk boundary so the turn aborts without leaking the worker thread or losing buffered output.

Cancellation is one-shot: once cancelled, it stays cancelled. Build a fresh token per turn rather than reusing across turns.

No Mutex on purpose. The flag is written exactly once (false -> true, never back) and only ever read otherwise — a single-writer, monotonic boolean. Under MRI’s GVL a lone ivar read/write is atomic, so no lock is needed for correctness. Critically, #cancel! runs from a SIGINT Signal.trap block, and Mutex#lock is forbidden in a trap context (Ruby bug #14222: “can’t be called from trap context”). A mutex here made the chat trap raise ThreadError, the flag never flipped, and the turn ran on. Keep this lock-free and trap-safe.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeCancelToken

Returns a new instance of CancelToken.



31
32
33
34
# File 'lib/rubino/interaction/cancel_token.rb', line 31

def initialize
  @cancelled = false
  @reason = :user
end

Instance Attribute Details

#reasonObject (readonly)

Why the turn was cancelled — distinguishes a deliberate user interrupt (Esc / Ctrl+C) from an EXTERNAL teardown (SIGTERM/SIGHUP from systemd, a terminal close, or a supervisor kill). Both unwind the turn the same way, but the result LABEL must not claim “interrupted by user” when no user interrupted (#361b). Defaults to :user — the overwhelmingly common case and the one the historical message described.



29
30
31
# File 'lib/rubino/interaction/cancel_token.rb', line 29

def reason
  @reason
end

Instance Method Details

#cancel!(reason: :user) ⇒ Object

reason records WHY: :user (Esc/Ctrl+C, default) or :external (SIGTERM/SIGHUP teardown). One-shot like @cancelled — the first reason wins, so a later cancel! can’t relabel a genuine user interrupt.



39
40
41
42
# File 'lib/rubino/interaction/cancel_token.rb', line 39

def cancel!(reason: :user)
  @reason = reason unless @cancelled
  @cancelled = true
end

#cancelled?Boolean

Returns:

  • (Boolean)


44
45
46
# File 'lib/rubino/interaction/cancel_token.rb', line 44

def cancelled?
  @cancelled
end

#check!Object

Raises Interrupted if the token has been cancelled. Used as a poll point inside hot loops (per-chunk in streams, per-iteration in the agent loop). The Interrupted carries a reason-appropriate message so an external-signal teardown is not mislabeled as a user interrupt (#361b).



52
53
54
55
56
# File 'lib/rubino/interaction/cancel_token.rb', line 52

def check!
  return unless cancelled?

  raise Rubino::Interrupted.new(reason: @reason)
end