Class: Kward::RPC::SessionManager

Inherits:
Object
  • Object
show all
Defined in:
lib/kward/rpc/session_manager.rb

Defined Under Namespace

Classes: RpcSession, Turn

Constant Summary collapse

RECENT_EVENT_LIMIT =
1_000
RPC_ATTACHMENT_MAX_BYTES =
10 * 1024 * 1024
RPC_IMAGE_MIME_TYPES =
["image/png", "image/jpeg", "image/gif", "image/webp"].freeze
STREAMING_BEHAVIORS =
["newTurn", "followUp", "steer"].freeze
1.0
WORKER_STOP =
Object.new.freeze

Instance Method Summary collapse

Constructor Details

#initialize(server:, client: Client.new, config_dir: ConfigFiles.config_dir, context_usage: ContextUsage.new, session_trash: SessionTrash.new) ⇒ SessionManager

Returns a new instance of SessionManager.



44
45
46
47
48
49
50
51
52
53
# File 'lib/kward/rpc/session_manager.rb', line 44

def initialize(server:, client: Client.new, config_dir: ConfigFiles.config_dir, context_usage: ContextUsage.new, session_trash: SessionTrash.new)
  @server = server
  @client = client
  @config_dir = config_dir
  @context_usage = context_usage
  @session_trash = session_trash
  @sessions = {}
  @turns = {}
  @mutex = Mutex.new
end

Instance Method Details

#answer_question(session_id:, question_request_id:, answers:) ⇒ Object



317
318
319
320
321
# File 'lib/kward/rpc/session_manager.rb', line 317

def answer_question(session_id:, question_request_id:, answers:)
  rpc_session = fetch_session(session_id)
  rpc_session.prompt.answer(question_request_id, answers)
  { ok: true }
end

#available_modelsObject



422
423
424
425
426
427
428
# File 'lib/kward/rpc/session_manager.rb', line 422

def available_models
  models = @client.respond_to?(:available_models) ? Array(@client.available_models) : []
  normalized = models.map { |model| normalize_model(model) }
  current = current_model
  normalized << current if normalized.none? { |model| model[:provider] == current[:provider] && model[:id] == current[:id] }
  normalized
end

#cancel_turn(turn_id:) ⇒ Object



293
294
295
296
297
298
299
300
301
302
# File 'lib/kward/rpc/session_manager.rb', line 293

def cancel_turn(turn_id:)
  turn = fetch_turn(turn_id)
  turn.cancel_requested = true
  turn.cancellation&.cancel!
  emit_turn_event(turn, "turnCancelRequested", {})
  if turn.status == "queued"
    finish_turn(turn, "canceled")
  end
  turn_payload(turn)
end

#cleanup_unused_sessionsObject



243
244
245
246
247
248
249
250
251
# File 'lib/kward/rpc/session_manager.rb', line 243

def cleanup_unused_sessions
  rpc_sessions = @mutex.synchronize { @sessions.values.dup }
  rpc_sessions.reverse_each do |rpc_session|
    next unless session_idle?(rpc_session)

    close_rpc_session(rpc_session)
  end
  { closed: true }
end

#clone_session(session_id:) ⇒ Object



111
112
113
114
115
116
117
118
119
# File 'lib/kward/rpc/session_manager.rb', line 111

def clone_session(session_id:)
  source = fetch_session(session_id)
  session, conversation = source.store.create_independent_from_conversation(source.conversation, parent_session: source.session)
  rpc_session = build_rpc_session(source.store, session, conversation, source.workspace_root)
  remember_session(rpc_session)
  cleanup_other_unused_sessions(rpc_session)
  emit_footer_update(rpc_session)
  session_payload(rpc_session)
end

#close_session(session_id:) ⇒ Object



237
238
239
240
241
# File 'lib/kward/rpc/session_manager.rb', line 237

def close_session(session_id:)
  rpc_session = fetch_session(session_id)
  close_rpc_session(rpc_session)
  { closed: true }
end

#compact_session(session_id:, custom_instructions: "") ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/kward/rpc/session_manager.rb', line 121

def compact_session(session_id:, custom_instructions: "")
  rpc_session = fetch_session(session_id)
  emit_session_event(rpc_session, "compactionStart", {})
  result = Compactor.new(conversation: rpc_session.conversation, client: @client, settings: compaction_settings).compact(custom_instructions: custom_instructions)
  payload = {
    summary: result.summary,
    firstKeptEntryId: result.first_kept_entry_id,
    tokensBefore: result.tokens_before,
    details: result.details
  }.compact
  emit_session_event(rpc_session, "compactionEnd", { result: payload, aborted: false, willRetry: false, errorMessage: nil })
  payload
rescue StandardError => e
  emit_session_event(rpc_session, "compactionEnd", { result: nil, aborted: true, willRetry: false, errorMessage: e.message }) if rpc_session
  raise e
end

#create_session(workspace_root: Dir.pwd, name: nil, resume_last: false) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/kward/rpc/session_manager.rb', line 55

def create_session(workspace_root: Dir.pwd, name: nil, resume_last: false)
  workspace_root = validate_workspace_root(workspace_root)
  store = SessionStore.new(config_dir: @config_dir, cwd: workspace_root)
  if resume_last && session_auto_resume_enabled? && name.to_s.strip.empty?
    path = store.remembered_last_session_path
    return resume_session(path: path, workspace_root: workspace_root, include_transcript: true) if path
  end

  conversation = new_conversation(workspace_root: workspace_root)
  session = store.create(model: conversation.model, reasoning_effort: conversation.reasoning_effort)
  session.rename(name) unless name.to_s.strip.empty?
  session.attach(conversation)
  rpc_session = build_rpc_session(store, session, conversation, workspace_root)
  remember_session(rpc_session)
  cleanup_other_unused_sessions(rpc_session)
  emit_footer_update(rpc_session)
  session_payload(rpc_session)
end

#current_modelObject



435
436
437
438
439
440
# File 'lib/kward/rpc/session_manager.rb', line 435

def current_model
  provider = @client.respond_to?(:current_provider) ? @client.current_provider : nil
  model = @client.respond_to?(:current_model) ? @client.current_model : nil
  context_window = @client.respond_to?(:current_context_window) ? @client.current_context_window : nil
  normalize_model(provider: provider, id: model, model: model, contextWindow: context_window, current: true)
end

#delete_session(session_id:) ⇒ Object



229
230
231
232
233
234
235
# File 'lib/kward/rpc/session_manager.rb', line 229

def delete_session(session_id:)
  rpc_session = fetch_session(session_id)
  path = rpc_session.session.path
  close_rpc_session(rpc_session, delete_unused: false)
  deleted = @session_trash.delete(path)
  { deleted: deleted, path: path }
end

#export_session(session_id:, path: nil, format: nil) ⇒ Object



220
221
222
223
224
225
226
227
# File 'lib/kward/rpc/session_manager.rb', line 220

def export_session(session_id:, path: nil, format: nil)
  rpc_session = fetch_session(session_id)
  format = export_format(format)
  path = export_path(rpc_session, path, format)
  content = export_content(rpc_session.conversation, format)
  File.write(path, content)
  { path: path, format: format }
end

#fork_messages(session_id:) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
# File 'lib/kward/rpc/session_manager.rb', line 138

def fork_messages(session_id:)
  rpc_session = fetch_session(session_id)
  {
    messages: tree_entries(rpc_session).filter_map do |record|
      message = record["message"]
      next unless message.is_a?(Hash) && message_role(message) == "user"

      { entryId: record["id"], text: display_message_text(message) }
    end
  }
end

#fork_session(session_id:, entry_id:) ⇒ Object

Raises:

  • (ArgumentError)


150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/kward/rpc/session_manager.rb', line 150

def fork_session(session_id:, entry_id:)
  source = fetch_session(session_id)
  entries = tree_entries(source)
  resolved_entry_id = resolve_tree_entry_id(entries, entry_id)
  selected_index = entries.index { |record| record["id"].to_s == resolved_entry_id.to_s }
  selected = selected_index && entries[selected_index]
  raise ArgumentError, "Unknown fork entryId: #{entry_id}" unless selected

  message = selected["message"]
  raise ArgumentError, "Entry is not forkable: #{entry_id}" unless message.is_a?(Hash) && message_role(message) == "user"

  session, conversation = source.store.create_independent_from_messages(
    entries[0...selected_index].filter_map { |record| record["message"] },
    model: source.conversation.model,
    reasoning_effort: source.conversation.reasoning_effort,
    parent_session: source.session
  )

  rpc_session = build_rpc_session(source.store, session, conversation, source.workspace_root)
  remember_session(rpc_session)
  cleanup_other_unused_sessions(rpc_session)
  {
    session: session_payload(rpc_session),
    text: full_message_text(message),
    cancelled: false
  }
end

#in_flight_steer_supported?Boolean

Returns:

  • (Boolean)


442
443
444
# File 'lib/kward/rpc/session_manager.rb', line 442

def in_flight_steer_supported?
  supports_in_flight_steer?
end

#list_sessions(workspace_root: Dir.pwd, limit: nil) ⇒ Object



96
97
98
99
100
101
102
103
# File 'lib/kward/rpc/session_manager.rb', line 96

def list_sessions(workspace_root: Dir.pwd, limit: nil)
  root = validate_workspace_root(workspace_root)
  store = SessionStore.new(config_dir: @config_dir, cwd: root)
  requested_limit = limit.to_i if limit
  requested_limit = nil unless requested_limit&.positive?
  store.recent(limit: requested_limit)
       .map { |info| session_info_payload(info, workspace_root: root) }
end

#memory_add(text:, scope: nil, tags: []) ⇒ Object



363
364
365
# File 'lib/kward/rpc/session_manager.rb', line 363

def memory_add(text:, scope: nil, tags: [])
  { memory: memory_manager.add_soft(text, scope: scope || "global", tags: tags) }
end

#memory_add_core(text:, scope: nil, tags: []) ⇒ Object



367
368
369
# File 'lib/kward/rpc/session_manager.rb', line 367

def memory_add_core(text:, scope: nil, tags: [])
  { memory: memory_manager.add_core(text, scope: scope || "global", tags: tags) }
end

#memory_auto_summary_disableObject



354
355
356
357
# File 'lib/kward/rpc/session_manager.rb', line 354

def memory_auto_summary_disable
  memory_manager.auto_summary_disable
  { autoSummary: false }
end

#memory_auto_summary_enableObject



349
350
351
352
# File 'lib/kward/rpc/session_manager.rb', line 349

def memory_auto_summary_enable
  memory_manager.auto_summary_enable
  { autoSummary: true }
end

#memory_disableObject



344
345
346
347
# File 'lib/kward/rpc/session_manager.rb', line 344

def memory_disable
  memory_manager.disable
  { enabled: false }
end

#memory_enableObject



339
340
341
342
# File 'lib/kward/rpc/session_manager.rb', line 339

def memory_enable
  memory_manager.enable
  { enabled: true }
end

#memory_forget(id:) ⇒ Object



371
372
373
# File 'lib/kward/rpc/session_manager.rb', line 371

def memory_forget(id:)
  { forgotten: memory_manager.forget_memory(id) }
end

#memory_inspectObject



383
384
385
# File 'lib/kward/rpc/session_manager.rb', line 383

def memory_inspect
  memory_manager.inspect_memory
end

#memory_list(include_inactive: false, workspace_root: Dir.pwd) ⇒ Object



359
360
361
# File 'lib/kward/rpc/session_manager.rb', line 359

def memory_list(include_inactive: false, workspace_root: Dir.pwd)
  memory_manager.hierarchy(include_inactive: include_inactive, workspace_root: workspace_root)
end

#memory_managerObject



330
331
332
# File 'lib/kward/rpc/session_manager.rb', line 330

def memory_manager
  Memory::Manager.for_config_dir(@config_dir)
end

#memory_promote(id:) ⇒ Object



375
376
377
# File 'lib/kward/rpc/session_manager.rb', line 375

def memory_promote(id:)
  { memory: memory_manager.promote_memory(id) }
end

#memory_relax(id:, workspace_root: Dir.pwd) ⇒ Object



379
380
381
# File 'lib/kward/rpc/session_manager.rb', line 379

def memory_relax(id:, workspace_root: Dir.pwd)
  { memory: memory_manager.relax_core(id, workspace_root: workspace_root) }
end

#memory_statusObject



334
335
336
337
# File 'lib/kward/rpc/session_manager.rb', line 334

def memory_status
  manager = memory_manager
  { enabled: manager.enabled?, autoSummary: manager.auto_summary_enabled?, paths: manager.paths }
end

#memory_summarize(session_id:) ⇒ Object



395
396
397
398
399
400
# File 'lib/kward/rpc/session_manager.rb', line 395

def memory_summarize(session_id:)
  rpc_session = fetch_session(session_id)
  records = memory_manager.summarize_conversation(rpc_session.conversation, client: @client)
  persist_memory_state(rpc_session)
  { memories: records }
end

#memory_why(session_id: nil) ⇒ Object



387
388
389
390
391
392
393
# File 'lib/kward/rpc/session_manager.rb', line 387

def memory_why(session_id: nil)
  if session_id
    rpc_session = fetch_session(session_id)
    return rpc_session.conversation.last_memory_retrieval || memory_manager.explain_retrieval
  end
  memory_manager.explain_retrieval
end

Raises:

  • (ArgumentError)


189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/kward/rpc/session_manager.rb', line 189

def navigate_tree(session_id:, entry_id:, summarize: false, custom_instructions: nil)
  rpc_session = fetch_session(session_id)
  entries = tree_entries(rpc_session)
  resolved_entry_id = resolve_tree_entry_id(entries, entry_id)
  entry = rpc_session.store.session_entry(rpc_session.session.path, resolved_entry_id)
  raise ArgumentError, "Unknown tree entryId: #{entry_id}" unless entry

  raise ArgumentError, "Tree entry is not selectable: #{entry_id}" unless selectable_tree_entry?(entry)

  message = entry["message"]
  user_entry = user_tree_entry?(entry)
  target_leaf = user_entry ? entry["parentId"] : entry["id"]
  editor_text = user_entry ? full_message_text(message) : nil
  previous_leaf = rpc_session.session.leaf_id

  if summarize
    summary = summarize_branch(rpc_session, from_id: previous_leaf, to_id: target_leaf, custom_instructions: custom_instructions)
    target_leaf = rpc_session.session.append_branch_summary(target_leaf, from_id: previous_leaf, summary: summary, details: {})
  else
    target_leaf ? rpc_session.session.branch(target_leaf) : rpc_session.session.reset_leaf
  end

  reload_rpc_session(rpc_session)
  {
    session: session_payload(rpc_session),
    editorText: editor_text,
    cancelled: false,
    aborted: false
  }.compact
end

#openrouter_catalogObject



430
431
432
433
# File 'lib/kward/rpc/session_manager.rb', line 430

def openrouter_catalog
  models = @client.respond_to?(:openrouter_catalog) ? Array(@client.openrouter_catalog) : []
  models.map { |model| normalize_model(model) }
end

#plugin_commandsObject



418
419
420
# File 'lib/kward/rpc/session_manager.rb', line 418

def plugin_commands
  plugin_registry.commands
end

#refresh_client_configObject



489
490
491
492
493
# File 'lib/kward/rpc/session_manager.rb', line 489

def refresh_client_config
  @client.reload_config if @client.respond_to?(:reload_config)
  refresh_session_runtime_contexts
  refresh_session_tool_registries
end

#reload_pluginsObject



495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/kward/rpc/session_manager.rb', line 495

def reload_plugins
  registry = PluginRegistry.load(reserved_commands: reserved_plugin_command_names)
  sessions = @mutex.synchronize do
    @plugin_registry = registry
    @sessions.values
  end
  sessions.each do |rpc_session|
    rpc_session.conversation.plugin_registry = registry if rpc_session.conversation.respond_to?(:plugin_registry=)
    rpc_session.conversation.refresh_system_message! if rpc_session.conversation.respond_to?(:refresh_system_message!)
    emit_footer_update(rpc_session)
  end
end

#rename_session(session_id:, name:) ⇒ Object



105
106
107
108
109
# File 'lib/kward/rpc/session_manager.rb', line 105

def rename_session(session_id:, name:)
  rpc_session = fetch_session(session_id)
  rpc_session.session.rename(name)
  session_payload(rpc_session)
end

#resume_session(path:, workspace_root: nil, include_transcript: false) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/kward/rpc/session_manager.rb', line 74

def resume_session(path:, workspace_root: nil, include_transcript: false)
  root = validate_workspace_root(workspace_root || Dir.pwd)
  store = SessionStore.new(config_dir: @config_dir, cwd: root)
  location = store.session_location(path)
  root = validate_workspace_root(location[:cwd])
  store = SessionStore.new(config_dir: @config_dir, cwd: root)
  session, conversation = store.load(
    location[:path],
    workspace: configured_workspace(root),
    model: current_model_id,
    reasoning_effort: current_reasoning_effort
  )
  rpc_session = build_rpc_session(store, session, conversation, root)
  remember_session(rpc_session)
  cleanup_other_unused_sessions(rpc_session)
  emit_footer_update(rpc_session)
  payload = session_payload(rpc_session)
  payload[:messages] = TranscriptNormalizer.new(rpc_session.conversation.messages).normalize if include_transcript
  payload[:resumed] = true
  payload
end

#run_command(session_id:, command:, arguments: "") ⇒ Object



323
324
325
326
327
328
# File 'lib/kward/rpc/session_manager.rb', line 323

def run_command(session_id:, command:, arguments: "")
  name = command.to_s.delete_prefix("/")
  return { ok: false, error: "unsupported", reason: "clientClipboardOwnedByUi" } if name == "copy"

  run_plugin_command(session_id: session_id, command: name, arguments: arguments)
end

#run_plugin_command(session_id:, command:, arguments: "") ⇒ Object



402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/kward/rpc/session_manager.rb', line 402

def run_plugin_command(session_id:, command:, arguments: "")
  rpc_session = fetch_session(session_id)
  command = plugin_registry.command_for(command.to_s.delete_prefix("/")) || raise(ArgumentError, "Unknown plugin command: #{command}")
  output = []
  context = PluginRegistry::Context.new(
    conversation: rpc_session.conversation,
    args: arguments.to_s,
    session: rpc_session.session,
    workspace_root: rpc_session.workspace_root,
    say_callback: lambda { |message| output << message.to_s }
  )
  result = command.handler.call(arguments.to_s, context)
  output = rpc_session.plugin_output.shift(rpc_session.plugin_output.length) + output
  { command: command.name, output: output, result: result.nil? ? nil : result.to_s }
end

#runtime_state(session_id:) ⇒ Object



446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'lib/kward/rpc/session_manager.rb', line 446

def runtime_state(session_id:)
  rpc_session = fetch_session(session_id)
  model = current_model
  compaction_settings = self.compaction_settings
  auto_compaction_reserve_tokens = compaction_reserve_tokens(
    context_window: model[:contextWindow],
    compaction_settings: compaction_settings
  )
  session = session_payload(rpc_session)
  RuntimePayloads.state(
    session: session,
    model: model,
    streaming: streaming?(rpc_session),
    steering_supported: supports_in_flight_steer?,
    auto_compaction_reserve_tokens: auto_compaction_reserve_tokens,
    active_persona_label: active_persona_label(rpc_session),
    message_count: message_count(rpc_session.conversation),
    pending_count: pending_turn_count(rpc_session.id),
    compaction_enabled: compaction_settings.enabled,
    workspace_guardrails_enabled: workspace_guardrails_enabled?
  )
end

#runtime_stats(session_id:) ⇒ Object



469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/kward/rpc/session_manager.rb', line 469

def runtime_stats(session_id:)
  rpc_session = fetch_session(session_id)
  session = session_payload(rpc_session)
  counts = message_stats(rpc_session.conversation)
  model = current_model
  compaction_settings = self.compaction_settings
  auto_compaction_reserve_tokens = compaction_reserve_tokens(
    context_window: model[:contextWindow],
    compaction_settings: compaction_settings
  )
  RuntimePayloads.stats(
    session: session,
    counts: counts,
    model: model,
    auto_compaction_reserve_tokens: auto_compaction_reserve_tokens,
    context_usage: context_usage(rpc_session, model),
    compaction_enabled: compaction_settings.enabled
  )
end

#session_modified_at(session) ⇒ Object



516
517
518
# File 'lib/kward/rpc/session_manager.rb', line 516

def session_modified_at(session)
  File.exist?(session.path) ? File.mtime(session.path) : nil
end

#session_payload(rpc_session) ⇒ Object



508
509
510
511
512
513
514
# File 'lib/kward/rpc/session_manager.rb', line 508

def session_payload(rpc_session)
  RuntimePayloads.session(
    rpc_session,
    modified_at: session_modified_at(rpc_session.session),
    active_persona_label: active_persona_label(rpc_session)
  )
end

#session_tree(session_id:) ⇒ Object



178
179
180
181
# File 'lib/kward/rpc/session_manager.rb', line 178

def session_tree(session_id:)
  rpc_session = fetch_session(session_id)
  { items: flatten_session_tree(rpc_session) }
end

#set_tree_label(session_id:, entry_id:, label: nil) ⇒ Object



183
184
185
186
187
# File 'lib/kward/rpc/session_manager.rb', line 183

def set_tree_label(session_id:, entry_id:, label: nil)
  rpc_session = fetch_session(session_id)
  rpc_session.session.append_label_change(entry_id, label)
  { ok: true }
end

#start_turn(session_id:, input:, streaming_behavior: nil, attachments: []) ⇒ Object



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/kward/rpc/session_manager.rb', line 258

def start_turn(session_id:, input:, streaming_behavior: nil, attachments: [])
  rpc_session = fetch_session(session_id)
  normalized_attachments = normalize_attachments(attachments)
  plugin_command, plugin_arguments = plugin_command_turn(input, normalized_attachments)
  display_input = input.to_s if input.is_a?(String)
  content = plugin_command ? input.to_s : user_turn_content(expand_prompt_input(input), normalized_attachments)
  streaming_behavior = validate_streaming_behavior(default_streaming_behavior(rpc_session, streaming_behavior), rpc_session: rpc_session)
  if streaming_behavior == "steer"
    steered_turn = steer_running_turn(rpc_session, content)
    return steered_turn if steered_turn

    streaming_behavior = "followUp"
  end
  turn = Turn.new(
    id: SecureRandom.uuid,
    session_id: rpc_session.id,
    input: content,
    display_input: display_input,
    status: "queued",
    cancel_requested: false,
    cancellation: Cancellation.new,
    created_at: now,
    events: [],
    next_sequence: 1,
    streaming_behavior: streaming_behavior,
    plugin_command_name: plugin_command&.name,
    plugin_arguments: plugin_arguments
  )
  @mutex.synchronize { @turns[turn.id] = turn }
  rpc_session.queue << turn.id
  ensure_worker(rpc_session)
  emit_turn_event(turn, "turnQueued", { status: "queued" })
  turn_payload(turn)
end

#transcript(session_id:) ⇒ Object



253
254
255
256
# File 'lib/kward/rpc/session_manager.rb', line 253

def transcript(session_id:)
  rpc_session = fetch_session(session_id)
  { session: session_payload(rpc_session), messages: TranscriptNormalizer.new(rpc_session.conversation.messages).normalize }
end

#turn_events(turn_id:, after_sequence: 0) ⇒ Object



308
309
310
311
312
313
314
315
# File 'lib/kward/rpc/session_manager.rb', line 308

def turn_events(turn_id:, after_sequence: 0)
  turn = fetch_turn(turn_id)
  after_sequence = after_sequence.to_i
  {
    turn: turn_payload(turn),
    events: turn.events.select { |event| event[:sequence].to_i > after_sequence }
  }
end

#turn_status(turn_id:) ⇒ Object



304
305
306
# File 'lib/kward/rpc/session_manager.rb', line 304

def turn_status(turn_id:)
  turn_payload(fetch_turn(turn_id))
end

#validate_workspace_root(root) ⇒ Object



520
521
522
523
524
525
# File 'lib/kward/rpc/session_manager.rb', line 520

def validate_workspace_root(root)
  expanded = File.expand_path(root.to_s.empty? ? Dir.pwd : root.to_s)
  raise "Workspace root is not an existing directory: #{expanded}" unless File.directory?(expanded)

  File.realpath(expanded)
end