Class: Session

Inherits:
ApplicationRecord show all
Includes:
AASM
Defined in:
app/models/session.rb

Overview

A conversation session — the fundamental unit of agent interaction. Owns an ordered stream of Message records representing everything that happened: user messages, agent responses, tool calls, etc.

Sessions form a hierarchy: a main session can spawn child sessions (sub-agents) that inherit the parent’s viewport context at fork time.

Defined Under Namespace

Classes: MissingSoulError

Constant Summary collapse

VIEW_MODES =
%w[basic verbose debug].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.system_prompt_payload(prompt, tools: nil) ⇒ Hash

Builds the system prompt payload for debug mode transmission. Token estimate covers both the system prompt and tool schemas since both consume the LLM’s context window. Tools are sent as raw schemas; the TUI formats them as TOON for display.

Parameters:

  • prompt (String)

    system prompt text

  • tools (Array<Hash>, nil) (defaults to: nil)

    tool schemas

Returns:

  • (Hash)

    payload with type, rendered debug content, and token estimate



644
645
646
647
648
649
650
651
652
653
654
655
656
# File 'app/models/session.rb', line 644

def self.system_prompt_payload(prompt, tools: nil)
  tools_json = tools&.any? ? tools.to_json : ""
  tokens = TokenEstimation.estimate_token_count(prompt.to_s + tools_json)

  debug = {role: :system_prompt, content: prompt, tokens: tokens, estimated: true}
  debug[:tools] = tools if tools&.any?

  {
    "id" => Message::SYSTEM_PROMPT_ID,
    "type" => "system_prompt",
    "rendered" => {"debug" => debug}
  }
end

Instance Method Details

#activate_skill(skill_name) ⇒ Skills::Definition

Activates a skill on this session by enqueuing its content as a PendingMessage that promotes to a ‘from_melete_skill` phantom pair. Skips re-activation while the previous phantom pair is still in the viewport — Aoide already has the skill text in front of her.

Parameters:

  • skill_name (String)

    name of the skill to activate

Returns:

Raises:



259
260
261
262
263
264
265
266
267
# File 'app/models/session.rb', line 259

def activate_skill(skill_name)
  definition = Skills::Registry.instance.find(skill_name)
  raise Skills::InvalidDefinitionError, "Unknown skill: #{skill_name}" unless definition
  return definition if active_skills.include?(skill_name)

  enqueue_recall_message("skill", skill_name, definition.content)
  Events::Bus.emit(Events::SkillActivated.new(session_id: id, skill_name: skill_name))
  definition
end

#activate_workflow(workflow_name) ⇒ Workflows::Definition

Activates a workflow on this session by enqueuing its content as a PendingMessage that promotes to a ‘from_melete_workflow` phantom tool pair. Workflows are main-session only. Skips re-activation while the previous phantom pair is still in the viewport.

Parameters:

  • workflow_name (String)

    name of the workflow to activate

Returns:

Raises:



278
279
280
281
282
283
284
285
286
# File 'app/models/session.rb', line 278

def activate_workflow(workflow_name)
  definition = Workflows::Registry.instance.find(workflow_name)
  raise Workflows::InvalidDefinitionError, "Unknown workflow: #{workflow_name}" unless definition
  return definition if active_workflow == workflow_name

  enqueue_recall_message("workflow", workflow_name, definition.content)
  Events::Bus.emit(Events::WorkflowActivated.new(session_id: id, workflow_name: workflow_name))
  definition
end

#active_skillsArray<String>

Active skills — skills Aoide is currently carrying or about to carry. Union of skills already promoted into the viewport and skills pending promotion. A skill is “active” from activation until eviction; there is no deactivation.

Returns:

  • (Array<String>)

    skill names, deduplicated, activation order first



219
220
221
222
# File 'app/models/session.rb', line 219

def active_skills
  queued = pending_messages.where(source_type: "skill").order(:id).pluck(:source_name)
  (skills_in_viewport + queued).uniq
end

#active_workflowString?

Active workflow — the workflow Aoide is currently carrying or about to carry. Pending activations take precedence over viewport contents (the last enqueue wins; the previous phantom pair evicts naturally).

Returns:

  • (String, nil)


229
230
231
232
# File 'app/models/session.rb', line 229

def active_workflow
  pending = pending_messages.where(source_type: "workflow").order(id: :desc).pick(:source_name)
  pending || workflow_in_viewport
end

#assemble_system_promptString

Assembles the system prompt: version preamble, soul, sisters block, available tools menu, tool guidelines, and snapshots. Skills, workflows, goals, and environment awareness flow through the message stream and tool responses, keeping the system prompt stable for prompt caching.

Returns:

  • (String)

    composed system prompt



295
296
297
298
299
300
301
302
303
304
# File 'app/models/session.rb', line 295

def assemble_system_prompt
  [
    assemble_version_preamble,
    assemble_soul_section,
    assemble_sisters_section,
    assemble_available_tools_section,
    assemble_tool_guidelines_section,
    assemble_snapshots_section
  ].compact.join("\n\n")
end

#broadcast_active_state!void

This method returns an undefined value.

Broadcasts current active skills and workflow to all subscribers. “Active” is viewport-derived — the HUD reflects what Aoide actually has in front of her. Callers invoke this after any operation that changes viewport composition (phantom pair promotion, Mneme eviction).



612
613
614
615
616
617
618
619
620
621
622
623
# File 'app/models/session.rb', line 612

def broadcast_active_state!
  ActionCable.server.broadcast("session_#{id}", {
    "action" => "active_skills_updated",
    "session_id" => id,
    "active_skills" => active_skills
  })
  ActionCable.server.broadcast("session_#{id}", {
    "action" => "active_workflow_updated",
    "session_id" => id,
    "active_workflow" => active_workflow
  })
end

#broadcast_children_update_to_parentvoid

This method returns an undefined value.

Broadcasts child session list to all clients subscribed to the parent session. Called when a child session is created or its AASM state changes so the HUD sub-agents section updates in real time. Evicted sub-agents (+hud_visible: false+) are filtered out — the panel mirrors what Aoide currently carries in her viewport.

Queries children via FK directly (avoids loading the parent record) and selects only the columns needed for the HUD payload.



483
484
485
486
487
488
489
490
491
492
493
494
495
496
# File 'app/models/session.rb', line 483

def broadcast_children_update_to_parent
  return unless parent_session_id

  children = Session.where(parent_session_id: parent_session_id, hud_visible: true)
    .order(:created_at)
    .select(:id, :name, :aasm_state)
  ActionCable.server.broadcast("session_#{parent_session_id}", {
    "action" => "children_updated",
    "session_id" => parent_session_id,
    "children" => children.map { |child|
      {"id" => child.id, "name" => child.name, "session_state" => child.aasm_state}
    }
  })
end

#broadcast_debug_context(system:, tools: nil) ⇒ void

This method returns an undefined value.

Broadcasts the full LLM debug context to debug-mode TUI clients. Called on every LLM request so the TUI shows exactly what the LLM receives — system prompt and tool schemas. No-op outside debug mode.

Parameters:

  • system (String, nil)

    the final system prompt sent to the LLM

  • tools (Array<Hash>, nil) (defaults to: nil)

    tool schemas sent to the LLM



600
601
602
603
604
# File 'app/models/session.rb', line 600

def broadcast_debug_context(system:, tools: nil)
  return unless view_mode == "debug" && system

  ActionCable.server.broadcast("session_#{id}", self.class.system_prompt_payload(system, tools: tools))
end

#clear_interrupt_flag_if_idlevoid

This method returns an undefined value.

AASM after_all_events callback — clears the interrupt_requested flag whenever the session lands in :idle. The flag is a one-shot signal that long-running tools (Tools::Bash) poll; once the round ends (tool aborted, response synthesized, drain wound down) the signal is spent and must not leak into the next round.



568
569
570
571
572
573
# File 'app/models/session.rb', line 568

def clear_interrupt_flag_if_idle
  return unless idle?
  return unless interrupt_requested?

  update_column(:interrupt_requested, false)
end

#create_user_message(content, source_type: nil, source_name: nil) ⇒ Message

Persists a user_message Message directly — skipping the PendingMessage mailbox. Used by DrainJob to finalize a promoted user_message PM and by the sub-agent spawn tools (Tools::SpawnSubagent, Tools::SpawnSpecialist) to seed the child’s conversation with its assigned task. The global Events::Subscribers::Persister skips user_message events, so these callers own the persistence.

Parameters:

  • content (String)

    user message text

  • source_type (String, nil) (defaults to: nil)

    origin type (e.g. “skill”, “workflow”) for viewport tracking; omitted for plain user messages

  • source_name (String, nil) (defaults to: nil)

    origin name (e.g. skill name)

Returns:

  • (Message)

    the persisted message record



461
462
463
464
465
466
467
468
469
470
471
# File 'app/models/session.rb', line 461

def create_user_message(content, source_type: nil, source_name: nil)
  now = now_ns
  payload = {type: "user_message", content: content, session_id: id, timestamp: now}
  payload["source_type"] = source_type if source_type
  payload["source_name"] = source_name if source_name
  messages.create!(
    message_type: "user_message",
    payload: payload,
    timestamp: now
  )
end

#effective_token_budgetInteger

Token budget appropriate for this session type. Sub-agents use a smaller budget to stay out of the “dumb zone”.

Returns:

  • (Integer)


130
131
132
# File 'app/models/session.rb', line 130

def effective_token_budget
  sub_agent? ? Anima::Settings.subagent_token_budget : Anima::Settings.token_budget
end

#emit_state_changevoid

This method returns an undefined value.

AASM after_all_events callback — publishes Events::SessionStateChanged so the broadcaster subscriber can keep the TUI spinner and parent-session HUD in sync with the state machine. Fires after the state column is updated and persisted, so aasm_state reliably holds the post-transition value.



557
558
559
# File 'app/models/session.rb', line 557

def emit_state_change
  Events::Bus.emit(Events::SessionStateChanged.new(session_id: id, state: aasm_state))
end

#enqueue_user_message(content, source_type: "user", source_name: nil, bounce_back: false) ⇒ PendingMessage

Enqueues an inbound human-side message (direct user input or a sub-agent reply) as an active PendingMessage. The PM’s after_create_commit emits the appropriate pipeline event when the session is idle (StartMelete for user input, StartProcessing for sub-agent deliveries). On a busy session the PM queues silently and #wake_drain_pipeline_if_pending picks it up on the next transition into :idle.

Parameters:

  • content (String)

    message text (raw, without attribution)

  • source_type (String) (defaults to: "user")

    origin type: “user” (default) or “subagent”

  • source_name (String, nil) (defaults to: nil)

    sub-agent nickname (required when source_type is “subagent”)

  • bounce_back (Boolean) (defaults to: false)

    when true, a failed first LLM call on the promoted message triggers a Events::BounceBack so the TUI can restore the text to the input field

Returns:



404
405
406
407
408
409
410
411
412
413
# File 'app/models/session.rb', line 404

def enqueue_user_message(content, source_type: "user", source_name: nil, bounce_back: false)
  message_type = (source_type == "subagent") ? "subagent" : "user_message"
  pending_messages.create!(
    content: content,
    source_type: source_type,
    source_name: source_name,
    message_type: message_type,
    bounce_back: bounce_back
  )
end

#eviction_zone_messagesActiveRecord::Relation<Message>

Returns the messages in the Mneme eviction zone — the oldest slice of the conversation starting from the boundary, filling the eviction budget walking newest-ward. These are the messages Mneme will summarize into a snapshot before advancing the boundary past them.

Mirror of #viewport_messages but walks oldest-first from the boundary instead of newest-first from the tail.

Returns:

  • (ActiveRecord::Relation<Message>)

    chronologically ordered by id



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'app/models/session.rb', line 172

def eviction_zone_messages
  return Message.none unless mneme_boundary_message_id

  budget = (Anima::Settings.token_budget * Anima::Settings.eviction_fraction).to_i

  scope = messages.where("messages.id >= ?", mneme_boundary_message_id)

  windowed = scope.select(
    "messages.*",
    "SUM(token_count) OVER (ORDER BY id ASC) AS running_total"
  )

  Message
    .from(windowed, :messages)
    .where("running_total <= ? OR running_total = token_count", budget)
    .order(:id)
end

#goals_summaryArray<Hash>

Serializes non-evicted goals as a lightweight summary for ActionCable broadcasts and TUI display. Returns a nested structure: root goals with their sub-goals inlined. Evicted goals and their sub-goals are excluded.

Returns:

  • (Array<Hash>)

    each with :id, :description, :status, and :sub_goals



312
313
314
# File 'app/models/session.rb', line 312

def goals_summary
  goals.root.not_evicted.includes(:sub_goals).order(:created_at).map(&:as_summary)
end

#heal_orphaned_tool_calls!Integer

Detects orphaned tool_call messages (those without a matching tool_response and whose timeout has expired) and creates synthetic error responses. An orphaned tool_call permanently breaks the session because the Anthropic API rejects conversations where a tool_use block has no matching tool_result.

Respects the per-call timeout stored in the tool_call message payload —a tool_call is only healed after its deadline has passed. This avoids prematurely healing long-running tools that the agent intentionally gave an extended timeout.

Returns:

  • (Integer)

    number of synthetic responses created



360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'app/models/session.rb', line 360

def heal_orphaned_tool_calls!
  current_ns = now_ns
  responded_ids = messages.where(message_type: "tool_response").select(:tool_use_id)
  unresponded = messages.where(message_type: "tool_call")
    .where.not(tool_use_id: responded_ids)

  healed = 0
  unresponded.find_each do |orphan|
    timeout = orphan.payload["timeout"] || Anima::Settings.tool_timeout
    deadline_ns = orphan.timestamp + (timeout * 1_000_000_000)
    next if current_ns < deadline_ns

    messages.create!(
      message_type: "tool_response",
      payload: {
        "type" => "tool_response",
        "content" => "Tool execution timed out after #{timeout} seconds — no result was returned.",
        "tool_name" => orphan.payload["tool_name"],
        "tool_use_id" => orphan.tool_use_id,
        "success" => false
      },
      tool_use_id: orphan.tool_use_id,
      timestamp: current_ns
    )
    healed += 1
  end
  healed
end

#initialize_mneme_boundary!void

This method returns an undefined value.

Places the initial Mneme boundary at the oldest eligible message in the session — the top of the raw window, from which Mneme will start compressing downward once that message drifts out of the viewport. Eligible messages are conversation messages (user/agent/system) and think tool_calls, considered on equal footing; bare tool_call or tool_response messages are never eligible.

No-op when the session has no eligible messages yet.



122
123
124
125
# File 'app/models/session.rb', line 122

def initialize_mneme_boundary!
  first_id = messages.conversation_or_think.order(:id).pick(:id)
  update_column(:mneme_boundary_message_id, first_id) if first_id
end

#messages_for_llm(token_budget: effective_token_budget) ⇒ Array<Hash>

Builds the message array expected by the Anthropic Messages API. Viewport layout (top to bottom):

[context prefix: goals + pinned messages] [sliding window messages]

Snapshots live in the system prompt (stable between Mneme runs). Goal events and recalled memories flow through the message stream as phantom tool pairs — they ride the conveyor belt as regular messages. After eviction, a goal snapshot + pinned messages block is rebuilt from DB state and prepended as a phantom pair.

The sliding window is post-processed by #ensure_atomic_tool_pairs which removes orphaned tool messages whose partner was cut off by the token budget.

Parameters:

  • token_budget (Integer) (defaults to: effective_token_budget)

    maximum tokens to include (positive)

Returns:

  • (Array<Hash>)

    Anthropic Messages API format



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'app/models/session.rb', line 332

def messages_for_llm(token_budget: effective_token_budget)
  heal_orphaned_tool_calls!

  sliding_budget = token_budget

  pinned_budget = (token_budget * Anima::Settings.mneme_pinned_budget_fraction).to_i
  sliding_budget -= pinned_budget

  window = viewport_messages(token_budget: sliding_budget).to_a
  first_message_id = window.first&.id

  prefix = assemble_context_prefix_messages(first_message_id, budget: pinned_budget)

  prefix + assemble_messages(ensure_atomic_tool_pairs(window))
end

#release_with_bounce_back(pm, error) ⇒ void

This method returns an undefined value.

Promotes a phantom-pair PendingMessage into a synthetic tool_call/tool_response Message pair — the LLM sees “a tool I invoked returned a result” and the pair rides the viewport like any real tool round. Used by DrainJob to flush background enrichment PMs (recalled memories, activated skills, workflow triggers, goal events, sub-agent deliveries) into the conversation.

Releases a failed drain claim and bounces the promoted user-message back to the client. Called from DrainJob when the LLM call raises before Events::LLMResponded ships. Destroying the exact message the PM promoted (tracked in PendingMessage#promoted_message_id) avoids the “last user_message” guess, which was racy under parallel drains.

Idempotent — a nil promoted_message_id skips the destroy and emits the BounceBack with message_id: nil so the TUI still restores input.

Parameters:

  • pm (PendingMessage)

    the user-message PM that failed to round-trip

  • error (Exception)

    the raised error



435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'app/models/session.rb', line 435

def release_with_bounce_back(pm, error)
  response_complete! if may_response_complete?

  bounced = pm.promoted_message_id && messages.find_by(id: pm.promoted_message_id)
  bounced&.destroy!

  Events::Bus.emit(Events::BounceBack.new(
    content: pm.content,
    error: error.message,
    session_id: id,
    message_id: bounced&.id
  ))
end

#schedule_mneme!void

This method returns an undefined value.

Checks whether the Mneme boundary has left the viewport and enqueues MnemeJob when it has. Delegates initial boundary placement to #initialize_mneme_boundary! on the first call.

The boundary has “left the viewport” when the cumulative token cost of everything from the boundary to the newest message exceeds the budget — a single SUM aggregate, no window function needed.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'app/models/session.rb', line 96

def schedule_mneme!
  return if sub_agent?

  if mneme_boundary_message_id.nil?
    initialize_mneme_boundary!
    return
  end

  tokens_since_boundary = messages
    .where("messages.id >= ?", mneme_boundary_message_id)
    .sum(:token_count)
  return if tokens_since_boundary <= effective_token_budget

  MnemeJob.perform_later(id)
end

#skills_in_viewportArray<String>

Names of skills currently present in the viewport as ‘from_melete_skill` phantom tool_call messages, in activation order.

Returns:

  • (Array<String>)

    skill names in the viewport, activation order



194
195
196
197
198
199
# File 'app/models/session.rb', line 194

def skills_in_viewport
  from_melete_messages
    .where("json_extract(payload, '$.tool_name') = ?", PendingMessage::MELETE_SKILL_TOOL)
    .pluck(Arel.sql("json_extract(payload, '$.tool_input.skill')"))
    .compact
end

#sub_agent?Boolean

Returns true if this session is a sub-agent (has a parent).

Returns:

  • (Boolean)

    true if this session is a sub-agent (has a parent)



83
84
85
# File 'app/models/session.rb', line 83

def sub_agent?
  parent_session_id.present?
end

#subagent_trace_in_viewport?(child) ⇒ Boolean

True when at least one of child‘s traces (the spawn_subagent tool pair or any from_{nickname} phantom pair) still lives above the Mneme boundary in this session’s viewport. Used by Mneme::Runner after boundary advancement to decide whether a child should drop out of the HUD panel.

Returns false when the given session isn’t a direct child, when it has no spawn_tool_use_id (legacy child), or when the boundary has passed every trace.

Parameters:

  • child (Session)

    a sub-agent session to check

Returns:

  • (Boolean)


510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
# File 'app/models/session.rb', line 510

def subagent_trace_in_viewport?(child)
  return false unless child.parent_session_id == id

  boundary_id = mneme_boundary_message_id
  scope = messages
  scope = scope.where("messages.id >= ?", boundary_id) if boundary_id

  spawn_uid = child.spawn_tool_use_id
  nickname = child.name
  conditions = []
  bindings = {}
  if spawn_uid
    conditions << "messages.tool_use_id = :uid"
    bindings[:uid] = spawn_uid
  end
  if nickname
    conditions << "json_extract(messages.payload, '$.tool_name') = :tool"
    bindings[:tool] = "from_#{nickname}"
  end
  return false if conditions.empty?

  scope.where(conditions.join(" OR "), **bindings).exists?
end

#system_promptString?

Returns the system prompt for this session. Sub-agent sessions use their stored prompt plus the pinned task. Main sessions assemble a full system prompt from soul, sisters, and snapshots. Skills, workflows, and goals are injected as phantom tool_use/tool_result pairs in the message stream (not here) to keep the system prompt stable for prompt caching. Environment awareness flows through Bash tool responses.

Returns:

  • (String, nil)

    the system prompt text, or nil when nothing to inject



243
244
245
246
247
248
249
# File 'app/models/session.rb', line 243

def system_prompt
  if sub_agent?
    [prompt, assemble_task_section].compact.join("\n\n")
  else
    assemble_system_prompt
  end
end

#tool_round_complete?Boolean

AASM guard for the executing → awaiting branch of start_processing. The round is complete when every orphan tool_call Message (one without a matching tool_response Message) has a corresponding tool_response PendingMessage waiting to be promoted. Until then the drain bails so the LLM never sees a half-assembled tool turn.

Returns:

  • (Boolean)


541
542
543
544
545
546
547
548
# File 'app/models/session.rb', line 541

def tool_round_complete?
  msg_responses = messages.where(message_type: "tool_response").select(:tool_use_id)
  pm_responses = pending_messages.where(message_type: "tool_response").select(:tool_use_id)
  messages.where(message_type: "tool_call")
    .where.not(tool_use_id: msg_responses)
    .where.not(tool_use_id: pm_responses)
    .none?
end

#tool_schemasArray<Hash>

Returns the deterministic tool schemas for this session’s type and granted_tools configuration. Standard and spawn tools are static class-level definitions — no ShellSession or registry needed. MCP tools are excluded (they require live server queries and appear after the first LLM request via #broadcast_debug_context).

Returns:

  • (Array<Hash>)

    tool schema hashes matching Anthropic tools API format



632
633
634
# File 'app/models/session.rb', line 632

def tool_schemas
  resolved_tool_classes.map(&:schema)
end

#viewport_messages(token_budget: effective_token_budget) ⇒ ActiveRecord::Relation<Message>

Returns the messages currently visible in the LLM context window as a composable AR relation. Selects own messages above the Mneme boundary whose cumulative token count (walked newest-first) fits within the budget. The newest message is always included even when it alone exceeds the budget. Messages are full-size or excluded entirely.

The selection runs as a single SQL query using a window function (OVER+). Older messages have been compressed into snapshots and no longer participate in the viewport. Pending messages live in a separate table (PendingMessage) and never appear here — they are promoted to real messages before the agent processes them.

Parameters:

  • token_budget (Integer) (defaults to: effective_token_budget)

    maximum tokens to include (positive)

Returns:

  • (ActiveRecord::Relation<Message>)

    chronologically ordered by id



148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'app/models/session.rb', line 148

def viewport_messages(token_budget: effective_token_budget)
  scope = messages
  scope = scope.where("messages.id >= ?", mneme_boundary_message_id) if mneme_boundary_message_id

  windowed = scope.select(
    "messages.*",
    "SUM(token_count) OVER (ORDER BY id DESC) AS running_total"
  )

  Message
    .from(windowed, :messages)
    .where("running_total <= ? OR running_total = token_count", token_budget)
    .order(:id)
end

#wake_drain_pipeline_if_pendingvoid

This method returns an undefined value.

AASM after_all_events callback — picks the oldest active PendingMessage and re-runs its pipeline routing whenever the session lands in :idle with work still queued. Covers the race where a PM arrives while the session is :awaiting (LLM in flight): its own after_create_commit saw may_start_processing? return false and emitted nothing, so without this callback the message would sit forever once the LLM call completed.

The :executing → :awaiting path (tool round close) does not need this callback — the closing tool_response PM is itself the wake.



587
588
589
590
591
# File 'app/models/session.rb', line 587

def wake_drain_pipeline_if_pending
  return unless idle?

  pending_messages.ordered_for_drain.first&.route_to_event_bus
end

#workflow_in_viewportString?

Workflow name currently present in the viewport as a ‘from_melete_workflow` phantom tool_call message, if any. The most recent activation wins when multiple are visible.

Returns:

  • (String, nil)

    workflow name in the viewport, or nil



206
207
208
209
210
211
# File 'app/models/session.rb', line 206

def workflow_in_viewport
  from_melete_messages
    .where("json_extract(payload, '$.tool_name') = ?", PendingMessage::MELETE_WORKFLOW_TOOL)
    .reorder(id: :desc)
    .pick(Arel.sql("json_extract(payload, '$.tool_input.workflow')"))
end