samagotchi 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -1
  3. data/docs/cli.md +4 -4
  4. data/docs/configuration.md +4 -0
  5. data/docs/hooks.md +1 -1
  6. data/docs/internals/engine.md +1 -1
  7. data/docs/plugins.md +1 -1
  8. data/docs/sessions.md +11 -4
  9. data/lib/samagotchi/bridge/card_store.rb +1 -1
  10. data/lib/samagotchi/bridge/turn_notice.rb +1 -1
  11. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  12. data/lib/samagotchi/bundles/mcp/plugin.rb +4 -3
  13. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +17 -4
  14. data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
  15. data/lib/samagotchi/bundles/system/delegated.md +1 -0
  16. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  17. data/lib/samagotchi/bundles/system/self_map.md +2 -1
  18. data/lib/samagotchi/child_reports.rb +214 -0
  19. data/lib/samagotchi/client.rb +35 -4
  20. data/lib/samagotchi/command_steps.rb +35 -15
  21. data/lib/samagotchi/config.rb +5 -0
  22. data/lib/samagotchi/engine.rb +2 -0
  23. data/lib/samagotchi/host_registry.rb +4 -0
  24. data/lib/samagotchi/idle_recap.rb +4 -0
  25. data/lib/samagotchi/kernel_loop.rb +63 -11
  26. data/lib/samagotchi/llm/chat_loop.rb +14 -7
  27. data/lib/samagotchi/llm/errors.rb +9 -0
  28. data/lib/samagotchi/log_subscriber.rb +1 -1
  29. data/lib/samagotchi/model_list_store.rb +117 -0
  30. data/lib/samagotchi/model_profile.rb +43 -0
  31. data/lib/samagotchi/prompt_warmup.rb +30 -5
  32. data/lib/samagotchi/reply_wait.rb +39 -8
  33. data/lib/samagotchi/send_command.rb +5 -3
  34. data/lib/samagotchi/session.rb +31 -5
  35. data/lib/samagotchi/session_inbox.rb +34 -1
  36. data/lib/samagotchi/session_manager.rb +41 -5
  37. data/lib/samagotchi/session_metrics.rb +6 -3
  38. data/lib/samagotchi/steer.rb +39 -7
  39. data/lib/samagotchi/terminal_ui/attached_loop.rb +4 -2
  40. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -1
  41. data/lib/samagotchi/terminal_ui/formatting.rb +38 -8
  42. data/lib/samagotchi/terminal_ui.rb +44 -5
  43. data/lib/samagotchi/tool_activity.rb +6 -2
  44. data/lib/samagotchi/tool_call_parser.rb +40 -0
  45. data/lib/samagotchi/tool_declarations.rb +7 -7
  46. data/lib/samagotchi/tools/delegate.rb +26 -13
  47. data/lib/samagotchi/tools/delegate_cursor.rb +91 -0
  48. data/lib/samagotchi/tools/delegate_wait.rb +50 -31
  49. data/lib/samagotchi/tools/execute.rb +3 -1
  50. data/lib/samagotchi/tools/task_runtime.rb +21 -2
  51. data/lib/samagotchi/tools/task_wait.rb +2 -2
  52. data/lib/samagotchi/version.rb +1 -1
  53. data/lib/samagotchi/web/app.rb +8 -2
  54. data/lib/samagotchi/web/public/activity.js +2 -1
  55. data/lib/samagotchi/web/public/app.js +21 -11
  56. data/lib/samagotchi/web/public/command_view.js +19 -5
  57. data/lib/samagotchi/web/public/format.js +8 -1
  58. data/lib/samagotchi/web/public/index.html +14 -6
  59. data/lib/samagotchi/web/public/notify.js +4 -2
  60. data/lib/samagotchi/web/public/stage_model.js +18 -0
  61. data/lib/samagotchi/web/public/stage_view.js +16 -11
  62. data/lib/samagotchi/web/public/task_stop.js +11 -2
  63. data/lib/samagotchi/web/public/timing.js +12 -7
  64. data/lib/samagotchi/web/public/turn_events.js +29 -9
  65. data/lib/samagotchi/web/public/turn_model.js +16 -11
  66. data/lib/samagotchi/web/public/turn_view.js +49 -7
  67. data/lib/samagotchi/worker.rb +240 -23
  68. metadata +4 -1
@@ -17,6 +17,7 @@ require_relative "continue_offer"
17
17
  require_relative "iteration_limit"
18
18
  require_relative "session_commands"
19
19
  require_relative "model_profile"
20
+ require_relative "child_reports"
20
21
  require_relative "tools/task_runtime"
21
22
 
22
23
  module Samagotchi
@@ -45,6 +46,11 @@ module Samagotchi
45
46
  # turn) is refused as busy.
46
47
  class Worker
47
48
  FALLBACK_TICK_SECONDS = 5
49
+ # A worker just started runs no wake turn for delegate reports this
50
+ # long: a worker spawned for a message (chi send to a stopped parent)
51
+ # gets it over the Bridge a moment after it starts, and that message's
52
+ # turn goes first (the reports join it).
53
+ WAKE_START_GRACE = 2.0
48
54
  # How much of a command's output goes into its :command_ran.
49
55
  COMMAND_OUTPUT_LIMIT = 4096
50
56
  BUSY_OUTPUT = "busy: wait for the turn to end"
@@ -89,6 +95,25 @@ module Samagotchi
89
95
  @discard = false
90
96
  # The exit asked for is a restart (POST /exit restart: true).
91
97
  @exit_restart = false
98
+ # The lines the running turn's drain merged ([prompt, origin]), for a
99
+ # failed prompt turn to hand back; reset as each turn begins.
100
+ @merged_this_turn = []
101
+ # A delegate child's side (ChildRing): whether the running turn is
102
+ # its parent's (it rings when it ends), whether that turn rang a
103
+ # question, and whether the next turn rings whatever its origin (it
104
+ # follows a question the parent was rung about: D10).
105
+ @reporting_turn = false
106
+ @asked_parent = false
107
+ @owes_parent = false
108
+ @initial_turn = false
109
+ # A parent's side: turns run for delegate reports since the last human
110
+ # input (session.max_wakes), whether a failed one paused them until
111
+ # then, whether the budget notice went out, and the reports a wake
112
+ # turn's first boundary hands over.
113
+ @wakes_in_a_row = 0
114
+ @wakes_paused = false
115
+ @budget_noticed = false
116
+ @wake_reports = nil
92
117
  end
93
118
 
94
119
  # Whether the session was empty as the worker left it, so the caller
@@ -96,6 +121,12 @@ module Samagotchi
96
121
  # checks again then).
97
122
  def discard? = @discard
98
123
 
124
+ # Whether delegate reports wait that an idle parent runs a turn for
125
+ # (#wake_state): SessionManager.run_session_loop starts a new worker
126
+ # for them after this one leaves. Reports the budget or a failure held
127
+ # back wait for the next human input instead.
128
+ def wake_due? = wake_state == :due
129
+
99
130
  # What a new session starts on (and /model resets to); nil before #run.
100
131
  attr_reader :default_model
101
132
 
@@ -105,6 +136,7 @@ module Samagotchi
105
136
  # (chi stop), :crashed when the loop raised (the session is marked
106
137
  # errored); SessionManager.run_session_loop turns it into the exit
107
138
  def run
139
+ @started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
108
140
  @session = Session.load(@session_id, state_dir: @state_dir)
109
141
  drop_dead_question
110
142
  @engine = build_engine
@@ -114,7 +146,7 @@ module Samagotchi
114
146
  @engine.session = @session
115
147
  # Input already waiting in the inbox starts the next turn at once: no
116
148
  # turn-end warm-up then.
117
- @engine.next_turn_waiting = -> { SessionInbox.find_new_input_files(@session_dir).any? }
149
+ @engine.next_turn_waiting = -> { SessionInbox.find_new_input_files(@session_dir).any? || wake_due? }
118
150
  @turn_flow = TurnFlow.new(engine: @engine)
119
151
  # Its question's answer is queued as the matching /continue (the
120
152
  # Bridge exists by the time anyone answers).
@@ -131,7 +163,11 @@ module Samagotchi
131
163
  @turn_end_seq = 0
132
164
  @engine.subscribe(observer: lambda { |event|
133
165
  @turn_end_seq = event[:event_seq] if Events::TURN_END.include?(event[:type])
166
+ @turn_id = event[:turn_id] if event[:type] == :turn_started
167
+ ring_question(event) if event[:type] == :question_requested
134
168
  })
169
+ # This session's own delegate children's news (rings in children/).
170
+ @child_reports = ChildReports.new(session_id: @session_id, state_dir: @state_dir)
135
171
  # /model's default is the config's, as in the REPL; the Engine started
136
172
  # on the session's model.
137
173
  @default_model = ModelProfile.required_model_name(nil)
@@ -190,17 +226,18 @@ module Samagotchi
190
226
  absorb_notes
191
227
 
192
228
  if (prompt = take_initial_prompt)
193
- run_prompt(prompt, nil) unless initial_command(prompt)
229
+ run_initial_prompt(prompt) unless initial_command(prompt)
194
230
  next
195
231
  end
196
232
 
197
233
  input_files = SessionInbox.find_new_input_files(@session_dir)
198
234
  if input_files.empty?
235
+ next if run_wake_turn
199
236
  next if run_due_reminders
200
237
  return left(:idle_exit) if @idle_exit.due? && leave_idle
201
238
  return left(@exit_restart ? :restart : :exit_requested) if @exit_requested && leave_on_request
202
239
 
203
- @waker.wait(@poll_interval)
240
+ @waker.wait(idle_wait)
204
241
  next
205
242
  end
206
243
 
@@ -216,6 +253,8 @@ module Samagotchi
216
253
  # Its stderr is /dev/null: the log is the only trace of why.
217
254
  Log.exception(:worker, "crashed", e)
218
255
  Session.mark_error(@session_id, reason: e.message, state_dir: @state_dir)
256
+ # A delegate's parent hears of it (best effort).
257
+ ChildRing.ring(@session, why: "crash", state_dir: @state_dir) if @session
219
258
  :crashed
220
259
  ensure
221
260
  # The anytime commands finish and the plugins' services stop (a
@@ -268,7 +307,7 @@ module Samagotchi
268
307
 
269
308
  Log.info(:worker, "dead_question_dropped", id: @session.pending_question[:id])
270
309
  @session.pending_question = nil
271
- @session.save(state_dir: @state_dir)
310
+ save_or_log(:dead_question) { @session.save(state_dir: @state_dir) }
272
311
  end
273
312
 
274
313
  # spawn_session hands the first prompt over in last_prompt, but
@@ -285,7 +324,7 @@ module Samagotchi
285
324
 
286
325
  prompt = @session.last_prompt
287
326
  @session.last_prompt = ""
288
- @session.save(state_dir: @state_dir)
327
+ save_or_log(:initial_prompt) { @session.save(state_dir: @state_dir) }
289
328
  prompt
290
329
  end
291
330
 
@@ -308,14 +347,16 @@ module Samagotchi
308
347
  return false unless queue_as_command(prompt, nil)
309
348
 
310
349
  @session.status = Session::STATUS_IDLE
311
- save_session
350
+ save_or_log(:initial_command) { save_session }
312
351
  true
313
352
  end
314
353
 
315
354
  # Add the queued context notes to the conversation (between turns only,
316
355
  # on this thread), save, then delete their files: a crash before the
317
356
  # delete leaves them claimed, and Engine#add_context_note skips a note
318
- # the saved conversation already holds. Not activity: a note alone
357
+ # the saved conversation already holds. A failed save keeps the files
358
+ # too: the next pass claims them again, finds them in the conversation
359
+ # and saves again. Not activity: a note alone
319
360
  # neither starts a turn nor keeps an idle worker up.
320
361
  def absorb_notes
321
362
  files = SessionInbox.find_new_note_files(@session_dir)
@@ -326,10 +367,20 @@ module Samagotchi
326
367
  note = SessionInbox.read_note(file)
327
368
  @engine.add_context_note(@session, note) if note
328
369
  end
329
- @session.save(state_dir: @state_dir)
370
+ return unless save_or_log(:notes) { @session.save(state_dir: @state_dir) }
371
+
330
372
  claimed.each { |file| FileUtils.rm_f(file) }
331
373
  end
332
374
 
375
+ # The first prompt (spawn_session's): a delegate child's task, which is
376
+ # its parent's although no client sent it.
377
+ def run_initial_prompt(prompt)
378
+ @initial_turn = true
379
+ run_prompt(prompt, nil)
380
+ ensure
381
+ @initial_turn = false
382
+ end
383
+
333
384
  def run_input_file(input_file)
334
385
  claimed_file = SessionInbox.claim_input_file(input_file)
335
386
  return unless claimed_file
@@ -352,7 +403,6 @@ module Samagotchi
352
403
  user_input(origin&.dig(:client_id))
353
404
  @continue_offer.drop(origin)
354
405
  @turn_flow.before_prompt_turn
355
- @merged_this_turn = []
356
406
  run_engine_turn(prompt, origin: origin, max_iterations: max_iterations(no_interrupt),
357
407
  images: images) do |result, error|
358
408
  if error
@@ -384,6 +434,85 @@ module Samagotchi
384
434
  true
385
435
  end
386
436
 
437
+ # :due when an idle parent should run a turn for its delegate
438
+ # children's reports now (session.delegate_reports: wake): rings wait,
439
+ # no continue offer does (they merge into its turn), wakes aren't paused
440
+ # by a failed one, and the wake budget isn't spent; :budget when only
441
+ # the budget stops it; nil otherwise.
442
+ def wake_state
443
+ return nil unless @child_reports && ChildRing.mode == "wake"
444
+ return nil if @wakes_paused || @turn_flow&.awaiting_continue?
445
+ return nil unless @child_reports.waiting?
446
+
447
+ @wakes_in_a_row < max_wakes ? :due : :budget
448
+ end
449
+
450
+ # Run a turn nobody typed for the reports the rings bring (a wake turn):
451
+ # a continue turn (no prompt bubble) whose first boundary merges them,
452
+ # with origin child:<id8>. A failure goes back to the checkpoint, keeps
453
+ # the rings and pauses wakes until a human's input (a provider outage
454
+ # doesn't loop).
455
+ # @return [Boolean] whether a wake turn ran
456
+ def run_wake_turn
457
+ return false if wake_grace_left.positive?
458
+
459
+ state = wake_state
460
+ budget_notice if state == :budget
461
+ return false unless state == :due
462
+
463
+ reports, = take_child_reports
464
+ # The turn must not generate with nothing to say: the drain gets this
465
+ # exact list at its first boundary.
466
+ return false if reports.empty?
467
+
468
+ @wake_reports = reports
469
+ @wakes_in_a_row += 1
470
+ Log.info(:worker, "delegate_wake_turn", reports: reports.size, in_a_row: @wakes_in_a_row)
471
+ @turn_flow.before_prompt_turn
472
+ run_engine_turn(nil, continue: true, origin: reports.first.origin, max_iterations: IterationLimit.for) do |result, error|
473
+ if error
474
+ note = TurnNote.failed(error.respond_to?(:summary) ? error.summary : error.message)
475
+ @turn_flow.prompt_turn_failed(note: note)
476
+ @wakes_paused = true
477
+ else
478
+ @continue_offer.after_turn(result)
479
+ end
480
+ end
481
+ true
482
+ ensure
483
+ @wake_reports = nil
484
+ end
485
+
486
+ def wake_grace_left
487
+ WAKE_START_GRACE - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at)
488
+ end
489
+
490
+ # The idle loop's sleep: the fallback tick, or less while reports wait
491
+ # out the start grace.
492
+ def idle_wait
493
+ left = wake_grace_left
494
+ left.positive? && wake_state == :due ? [left, @poll_interval].min : @poll_interval
495
+ end
496
+
497
+ def max_wakes
498
+ value = Integer(Config.get(ChildRing::MAX_WAKES_KEY), exception: false)
499
+ value&.positive? ? value : ChildRing::MAX_WAKES_DEFAULT
500
+ rescue StandardError
501
+ ChildRing::MAX_WAKES_DEFAULT
502
+ end
503
+
504
+ # The wake budget is spent: the parent's UIs hear once that reports
505
+ # wait for the next message.
506
+ def budget_notice
507
+ return if @budget_noticed
508
+
509
+ @budget_noticed = true
510
+ count = SessionInbox.find_ring_files(@session_dir).map { |f| SessionInbox.read_ring(f)&.dig(:child_id) }.uniq.size
511
+ @engine.announce(type: :hook_notice, hook: "delegate", level: :info, between_turns: true,
512
+ text: "#{count} delegate report#{"s" if count != 1} waiting; #{count == 1 ? "it joins" : "they join"} " \
513
+ "your next message (#{max_wakes} turn#{"s" if max_wakes != 1} ran for reports in a row, #{ChildRing::MAX_WAKES_KEY})")
514
+ end
515
+
387
516
  def max_iterations(no_interrupt) = IterationLimit.for(no_interrupt: no_interrupt)
388
517
 
389
518
  # @return [Boolean] whether any command ran
@@ -426,7 +555,7 @@ module Samagotchi
426
555
  announce_command(command, status: result.status.to_s, output: result.output, changed: Array(result.changed))
427
556
  shown.each { |event| @engine.announce(event) }
428
557
  end
429
- save_session unless Array(result.changed).empty?
558
+ save_or_log(:command) { save_session } unless Array(result.changed).empty?
430
559
  user_input(command[:client_id]) if resolved
431
560
  @continue_offer.after_command(result, resolved: resolved)
432
561
  @continue_offer.run_continue_turn(command) if result.resume
@@ -493,6 +622,10 @@ module Samagotchi
493
622
  # @yieldparam result [Object, nil] Engine#run_turn's, nil on a failure
494
623
  # @yieldparam error [StandardError, nil]
495
624
  def run_engine_turn(prompt, **turn_args)
625
+ # Every kind of turn merges steering (a reminder turn too, which may be
626
+ # a fresh worker's first), so the list is this turn's from the start.
627
+ @merged_this_turn = []
628
+ @reporting_turn = reports_to_parent?(turn_args[:origin])
496
629
  begin
497
630
  @session.status = Session::STATUS_RUNNING
498
631
  @session.save(state_dir: @state_dir)
@@ -515,16 +648,66 @@ module Samagotchi
515
648
  unless response.nil? || response.strip.empty? || @continue_offer.awaiting?
516
649
  SessionInbox.write_output(@session_dir, response)
517
650
  end
518
- save_after_turn
519
- end
520
-
521
- # The turn's own save: the turn ended fine and its messages are in
522
- # memory, so a save that fails here (disk full, permissions; the next
523
- # save writes them) must not take the worker down with it.
524
- def save_after_turn
525
- save_session
651
+ save_or_log(:turn) { save_session }
652
+ settle_child_reports(error)
653
+ ring_parent_after_turn
654
+ end
655
+
656
+ # The delegate reports this turn took: a kept turn moves the cursors on
657
+ # and deletes their rings; a failed one (rolled back) leaves the rings
658
+ # for the next turn.
659
+ def settle_child_reports(error)
660
+ error ? @child_reports.release : @child_reports.commit
661
+ end
662
+
663
+ # A delegate child's turn that was its parent's (#reports_to_parent?)
664
+ # rings the parent, after the save: the parent reads a settled child.
665
+ # A question it rang about that still waits makes the next turn ring
666
+ # too, whoever starts it (the user answering Continue on the web).
667
+ def ring_parent_after_turn
668
+ ChildRing.ring(@session, why: "turn_end", state_dir: @state_dir) if @reporting_turn
669
+ @owes_parent = @asked_parent && !@session.pending_question.nil?
670
+ @asked_parent = false
671
+ @reporting_turn = false
672
+ end
673
+
674
+ # Whether the turn starting now is a delegate child's parent's: its
675
+ # task (the initial prompt), a follow-up, a continue the parent
676
+ # answered, a reminder, anything but a human typing into the child
677
+ # (ArchiveStore.user_input?), and any turn after a question the parent
678
+ # was rung about.
679
+ def reports_to_parent?(origin)
680
+ return false unless @session.delegate?
681
+
682
+ @initial_turn || @owes_parent || !ArchiveStore.user_input?(origin&.dig(:client_id))
683
+ end
684
+
685
+ # The Engine published a question (after the question desk saved it).
686
+ # In a turn that reports to the parent, the model's own question and
687
+ # the step-limit continue ring it; approvals and hooks' questions are
688
+ # the user's, on this session's card.
689
+ def ring_question(event)
690
+ return unless @reporting_turn
691
+
692
+ kind = event.dig(:pending_question, :kind).to_s
693
+ return unless ["", ContinueOffer::KIND].include?(kind)
694
+
695
+ ChildRing.ring(@session, why: "question", state_dir: @state_dir)
696
+ @asked_parent = true
697
+ end
698
+
699
+ # A save between turns (the turn's own, a command's, the notes', the
700
+ # first prompt's): what it saves is in memory, so one that fails (disk
701
+ # full, permissions; the next save writes it) is logged and must not
702
+ # take the worker down with it.
703
+ # @param at [Symbol] which save, for the log
704
+ # @return [Boolean] whether it saved
705
+ def save_or_log(at)
706
+ yield
707
+ true
526
708
  rescue SystemCallError, IOError => e
527
- Log.exception(:worker, "save_failed", e)
709
+ Log.exception(:worker, "save_failed", e, at: at)
710
+ false
528
711
  end
529
712
 
530
713
  # Back to the conversation before the failed turn, as the REPL does (so
@@ -580,14 +763,42 @@ module Samagotchi
580
763
  end
581
764
  end
582
765
  merged.each { |_prompt, origin| user_input(origin&.dig(:client_id)) }
583
- unless merged.empty?
584
- @engine.announce(type: :input_merged, count: merged.size, origins: merged.filter_map(&:last))
766
+ # Delegate children's reports: never handed back on a failure
767
+ # (their rings stay for the next turn), so not in @merged_this_turn.
768
+ reports, mark = take_child_reports
769
+ unless merged.empty? && reports.empty?
770
+ @engine.announce(type: :input_merged, count: merged.size + reports.size,
771
+ origins: merged.filter_map(&:last) + reports.map(&:origin))
585
772
  @merged_this_turn.concat(merged)
586
773
  end
587
774
  # Each line keeps its sender (Steer.source_for_client): a chi send or
588
775
  # a delegating parent's line is saved and shown to the model as theirs.
589
- merged.map { |prompt, origin| Steer::Line.new(text: prompt, source: Steer.source_for_client(origin&.dig(:client_id))) }
776
+ lines = merged.map { |prompt, origin| Steer::Line.new(text: prompt, source: Steer.source_for_client(origin&.dig(:client_id))) } +
777
+ reports.map(&:line)
778
+ # A wake turn starts with its first line, whoever sent it.
779
+ lines[0] = lines[0].with(mark: mark) if mark && lines.any?
780
+ lines
781
+ end
782
+ end
783
+
784
+ # The reports for rings that came in since the turn last looked. A
785
+ # failure to read them must not take the turn's steering with it.
786
+ # A wake turn's first call gets the reports it was started for (their
787
+ # message is the turn's start: turn_start, turn_id, so a reload shows
788
+ # the turn as its own).
789
+ # @return [Array(Array<ChildReports::Report>, Hash|nil)] the reports
790
+ # and the mark of their message
791
+ def take_child_reports
792
+ if @wake_reports
793
+ reports = @wake_reports
794
+ @wake_reports = nil
795
+ return [reports, { turn_start: true, turn_id: @turn_id }.compact]
590
796
  end
797
+
798
+ [@child_reports.take, nil]
799
+ rescue StandardError => e
800
+ Log.warn(:worker, "delegate_reports_failed", error: e.class.name, msg: e.message)
801
+ [[], nil]
591
802
  end
592
803
 
593
804
  # Check again with the event log held, which the Bridge holds while it
@@ -688,8 +899,14 @@ module Samagotchi
688
899
 
689
900
  # A human's input (ArchiveStore.user_input?) un-archives the session;
690
901
  # a delegate's, a plugin's or a reminder's doesn't.
902
+ # It also resets the delegate-report wakes (budget and pause).
691
903
  def user_input(client_id)
692
- ArchiveStore.user_input(@session_id, state_dir: @state_dir) if ArchiveStore.user_input?(client_id)
904
+ return unless ArchiveStore.user_input?(client_id)
905
+
906
+ ArchiveStore.user_input(@session_id, state_dir: @state_dir)
907
+ @wakes_in_a_row = 0
908
+ @wakes_paused = false
909
+ @budget_noticed = false
693
910
  end
694
911
 
695
912
  def stopped_on_disk?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: samagotchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.0
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -193,6 +193,7 @@ files:
193
193
  - lib/samagotchi/bundles/system/memory_guide.md
194
194
  - lib/samagotchi/bundles/system/self_map.md
195
195
  - lib/samagotchi/cancellation_controller.rb
196
+ - lib/samagotchi/child_reports.rb
196
197
  - lib/samagotchi/cli/command.rb
197
198
  - lib/samagotchi/cli/exit.rb
198
199
  - lib/samagotchi/cli/flags.rb
@@ -301,6 +302,7 @@ files:
301
302
  - lib/samagotchi/memory_bundle/uninstaller.rb
302
303
  - lib/samagotchi/memory_paths.rb
303
304
  - lib/samagotchi/model_catalog.rb
305
+ - lib/samagotchi/model_list_store.rb
304
306
  - lib/samagotchi/model_overlay.rb
305
307
  - lib/samagotchi/model_profile.rb
306
308
  - lib/samagotchi/model_ref.rb
@@ -393,6 +395,7 @@ files:
393
395
  - lib/samagotchi/tools/builtins.rb
394
396
  - lib/samagotchi/tools/cancel_reminder.rb
395
397
  - lib/samagotchi/tools/delegate.rb
398
+ - lib/samagotchi/tools/delegate_cursor.rb
396
399
  - lib/samagotchi/tools/delegate_relay.rb
397
400
  - lib/samagotchi/tools/delegate_result.rb
398
401
  - lib/samagotchi/tools/delegate_wait.rb