Class: Ace::Hitl::Molecules::LabProjectionObserver

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/hitl/molecules/lab_projection_observer.rb

Overview

Observes the Lab request public projection (<public-dir>/<request-id>.json) so waiters can surface lab-side states instead of hanging blind. Read-only; consumption of the answer relay remains the caller's choice.

Projection schema (deployed lab relay, lab-config 8wl.t.ga9): the lifecycle field state carries created / answer-delivered / consumed / cancelled, while effect-callback outcomes live in the SEPARATE effect_state field (callback-pending-with-answer / callback-ok / callback-escalated). Both fields are observed here.

Defined Under Namespace

Classes: Snapshot

Constant Summary collapse

DEFAULT_PUBLIC_DIR =
"/run/lab/hitl/public"
PUBLIC_DIR_ENV =

Read-side override for waiters. The lifecycle store writes the projection under ACE_HITL_STORE_ROOT/public — the same deployed default directory. The two env vars overlap by default: the store root owns the WRITE side (and the store's other directories), this one only redirects WHERE WAITERS READ the public projection from (e.g. a host that mounts the projection read-only). Keep them pointing at the same directory unless a projection relay is deliberately placed in between.

"ACE_HITL_LAB_PUBLIC_DIR"
LIFECYCLE_TERMINAL_STATES =

Lifecycle (state) terminal values: the answer was delivered to the relay, consumed, or the request was cancelled.

%w[answer-delivered consumed cancelled].freeze
EFFECT_TERMINAL_STATES =

Effect (effect_state) terminal values: the callback reached a verdict. callback-pending-with-answer keeps a waiter holding.

%w[callback-ok callback-escalated].freeze

Instance Method Summary collapse

Constructor Details

#initialize(public_dir: nil) ⇒ LabProjectionObserver

Returns a new instance of LabProjectionObserver.



41
42
43
# File 'lib/ace/hitl/molecules/lab_projection_observer.rb', line 41

def initialize(public_dir: nil)
  @public_dir = public_dir
end

Instance Method Details

#effective_state(snapshot) ⇒ Object

The state a waiter should report: the effect outcome when one exists (so lab_request_state never claims plain answer-delivered while an effect outcome is present), the lifecycle state otherwise.



65
66
67
68
69
# File 'lib/ace/hitl/molecules/lab_projection_observer.rb', line 65

def effective_state(snapshot)
  return nil unless snapshot

  snapshot.effect_state || snapshot.state
end

#projection_path(request_id) ⇒ Object



85
86
87
# File 'lib/ace/hitl/molecules/lab_projection_observer.rb', line 85

def projection_path(request_id)
  File.join(public_dir, "#{request_id}.json")
end

#snapshot_for(request_id) ⇒ Object

Reads the projection and returns a Snapshot with the lifecycle state and the separate effect_state, or nil when the projection is missing or unreadable.



48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/ace/hitl/molecules/lab_projection_observer.rb', line 48

def snapshot_for(request_id)
  path = projection_path(request_id)
  return nil unless File.file?(path)

  projection = JSON.parse(File.read(path))
  Snapshot.new(
    state: projection["state"],
    effect_state: projection["effect_state"]
  )
rescue
  nil
end

#terminal?(snapshot, effect_declared: false) ⇒ Boolean

Terminality depends on whether the request declared an effect callback: effect-declaring requests terminate only on an effect verdict (callback-ok / callback-escalated), never at answer delivery; other requests terminate on lifecycle terminal states.

Returns:

  • (Boolean)


75
76
77
78
79
80
81
82
83
# File 'lib/ace/hitl/molecules/lab_projection_observer.rb', line 75

def terminal?(snapshot, effect_declared: false)
  return false unless snapshot

  if effect_declared
    EFFECT_TERMINAL_STATES.include?(snapshot.effect_state)
  else
    LIFECYCLE_TERMINAL_STATES.include?(snapshot.state)
  end
end