samagotchi 0.18.1 → 0.19.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +85 -1
  3. data/README.md +4 -3
  4. data/docs/architecture.md +15 -2
  5. data/docs/cli.md +34 -8
  6. data/docs/configuration.md +8 -4
  7. data/docs/memory.md +4 -0
  8. data/docs/sessions.md +6 -4
  9. data/lib/samagotchi/bridge.rb +25 -9
  10. data/lib/samagotchi/bridge_client/event_stream.rb +37 -1
  11. data/lib/samagotchi/bridge_client.rb +9 -5
  12. data/lib/samagotchi/bundle_command.rb +2 -1
  13. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +7 -4
  14. data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
  15. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  16. data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
  17. data/lib/samagotchi/engine.rb +9 -3
  18. data/lib/samagotchi/guardrail_wiring.rb +8 -2
  19. data/lib/samagotchi/guardrails/approval.rb +10 -4
  20. data/lib/samagotchi/guardrails/approvals.rb +60 -12
  21. data/lib/samagotchi/host_registry.rb +22 -4
  22. data/lib/samagotchi/idle_scheduler.rb +1 -1
  23. data/lib/samagotchi/installed_versions.rb +91 -0
  24. data/lib/samagotchi/live_versions.rb +5 -3
  25. data/lib/samagotchi/memory_bundle/builder.rb +24 -1
  26. data/lib/samagotchi/memory_bundle/index_sync.rb +1 -9
  27. data/lib/samagotchi/memory_bundle/installer.rb +33 -0
  28. data/lib/samagotchi/memory_bundle/status.rb +5 -1
  29. data/lib/samagotchi/memory_bundle/uninstaller.rb +6 -2
  30. data/lib/samagotchi/model_overlay.rb +39 -0
  31. data/lib/samagotchi/model_profile.rb +4 -0
  32. data/lib/samagotchi/relay_desk.rb +9 -0
  33. data/lib/samagotchi/session_manager.rb +166 -11
  34. data/lib/samagotchi/session_metrics.rb +36 -13
  35. data/lib/samagotchi/sessions_command.rb +43 -4
  36. data/lib/samagotchi/system_prompt.rb +10 -1
  37. data/lib/samagotchi/terminal_ui/attached_loop.rb +48 -3
  38. data/lib/samagotchi/terminal_ui/image_input.rb +11 -4
  39. data/lib/samagotchi/terminal_ui/version_lines.rb +48 -0
  40. data/lib/samagotchi/tools/builtins.rb +21 -5
  41. data/lib/samagotchi/turn_note.rb +3 -2
  42. data/lib/samagotchi/update_command.rb +33 -12
  43. data/lib/samagotchi/version.rb +1 -1
  44. data/lib/samagotchi/web/app.rb +64 -11
  45. data/lib/samagotchi/web/public/app.js +102 -29
  46. data/lib/samagotchi/web/public/data.js +6 -0
  47. data/lib/samagotchi/web/public/format.js +4 -2
  48. data/lib/samagotchi/web/public/index.html +11 -0
  49. data/lib/samagotchi/web/public/question_card.js +12 -0
  50. data/lib/samagotchi/web/public/question_cards.js +17 -11
  51. data/lib/samagotchi/web/public/scope.js +15 -6
  52. data/lib/samagotchi/web/public/session_state.js +4 -0
  53. data/lib/samagotchi/web/public/turn_events.js +107 -3
  54. data/lib/samagotchi/web/public/update_notice.js +87 -14
  55. data/lib/samagotchi/web/server.rb +28 -1
  56. data/lib/samagotchi/web/session_hub.rb +30 -3
  57. data/lib/samagotchi/web/session_summary.rb +5 -0
  58. data/lib/samagotchi/worker.rb +25 -8
  59. data/lib/samagotchi/worker_idle_exit.rb +31 -2
  60. data/lib/samagotchi/worker_sidecar.rb +7 -5
  61. metadata +3 -1
@@ -16,6 +16,7 @@ require_relative "continue_offer"
16
16
  require_relative "iteration_limit"
17
17
  require_relative "session_commands"
18
18
  require_relative "model_profile"
19
+ require_relative "tools/task_runtime"
19
20
 
20
21
  module Samagotchi
21
22
  # The loop of a background session worker, once it owns the session (see
@@ -85,6 +86,8 @@ module Samagotchi
85
86
  @exit_requested_by = nil
86
87
  # Set as it leaves: nothing happened in the session (#discard?).
87
88
  @discard = false
89
+ # The exit asked for is a restart (POST /exit restart: true).
90
+ @exit_restart = false
88
91
  end
89
92
 
90
93
  # Whether the session was empty as the worker left it, so the caller
@@ -96,7 +99,8 @@ module Samagotchi
96
99
  attr_reader :default_model
97
100
 
98
101
  # @return [Symbol] :idle_exit, :exit_requested when a client asked it
99
- # to exit (Bridge POST /exit), :stopped when the session was stopped
102
+ # to exit (Bridge POST /exit), :restart when it asked for a restart
103
+ # (the caller starts a new worker), :stopped when the session was stopped
100
104
  # (chi stop), :crashed when the loop raised (the session is marked
101
105
  # errored); SessionManager.run_session_loop turns it into the exit
102
106
  def run
@@ -154,7 +158,8 @@ module Samagotchi
154
158
  engine: @engine, bridge: @bridge,
155
159
  timeout_minutes: @idle_exit_minutes || SessionManager.config_idle_exit_minutes,
156
160
  input_pending: -> { !@command_queue.empty? || !SessionInbox.find_new_input_files(@session_dir).empty? },
157
- awaiting_continue: -> { @turn_flow.awaiting_continue? }
161
+ awaiting_continue: -> { @turn_flow.awaiting_continue? },
162
+ running_tasks: -> { Tools::TaskRuntime.running_created_in(@engine.messages_checkpoint) }
158
163
  )
159
164
 
160
165
  begin
@@ -188,7 +193,7 @@ module Samagotchi
188
193
  if input_files.empty?
189
194
  next if run_due_reminders
190
195
  return left(:idle_exit) if @idle_exit.due? && leave_idle
191
- return left(:exit_requested) if @exit_requested && leave_on_request
196
+ return left(@exit_restart ? :restart : :exit_requested) if @exit_requested && leave_on_request
192
197
 
193
198
  @waker.wait(@poll_interval)
194
199
  next
@@ -581,17 +586,20 @@ module Samagotchi
581
586
 
582
587
  # Bridge POST /exit, on a Bridge thread with the event log held.
583
588
  # +delete+: the session is deleted after (/exit --delete), so no recap.
589
+ # +restart+: a new worker takes over (on the newest chi installed), so
590
+ # WorkerIdleExit#hold_for_restart's rules, and no recap or discard.
584
591
  # @return [Symbol, nil] what keeps the worker up, nil when it will leave
585
- def exit_request(client_id, delete: false)
592
+ def exit_request(client_id, delete: false, restart: false)
586
593
  # The Bridge serves before the idle-exit policy exists.
587
594
  return :starting unless @idle_exit
588
595
 
589
- hold = @idle_exit.hold_for_request(requester: client_id)
596
+ hold = restart ? @idle_exit.hold_for_restart : @idle_exit.hold_for_request(requester: client_id)
590
597
  return hold if hold
591
598
 
592
599
  @exit_requested = true
593
600
  @exit_requested_by = client_id
594
- @exit_deletes = delete
601
+ @exit_deletes = delete && !restart
602
+ @exit_restart = restart
595
603
  @waker.wake
596
604
  nil
597
605
  end
@@ -604,16 +612,21 @@ module Samagotchi
604
612
  # @return [Boolean] false when something came in since the request
605
613
  def leave_on_request
606
614
  @engine.synchronize_events do
607
- hold = @idle_exit.hold_for_request(requester: @exit_requested_by, streams: false)
615
+ hold = if @exit_restart
616
+ @idle_exit.hold_for_restart
617
+ else
618
+ @idle_exit.hold_for_request(requester: @exit_requested_by, streams: false)
619
+ end
608
620
  if hold
609
621
  @exit_requested = nil
622
+ @exit_restart = false
610
623
  Log.info(:worker, "exit_held", reason: hold)
611
624
  next false
612
625
  end
613
626
 
614
627
  @bridge&.stop
615
628
  @engine.stop_idle
616
- Log.info(:worker, "exit_requested", by: @exit_requested_by || "a client")
629
+ Log.info(:worker, @exit_restart ? "restart_requested" : "exit_requested", by: @exit_requested_by || "a client")
617
630
  true
618
631
  end
619
632
  end
@@ -623,7 +636,11 @@ module Samagotchi
623
636
  # stopped by now, and nothing waits on this (the TUI has detached). A
624
637
  # `chi send` meanwhile is picked up after the exit (run_session_loop);
625
638
  # a `chi --attach` finds no Bridge until then.
639
+ # A restart keeps even an empty session (a new worker takes it over at
640
+ # once) and leaves the recap to the conversation's next real pause.
626
641
  def left(result)
642
+ return result if result == :restart
643
+
627
644
  @discard = empty_session?
628
645
  write_recap_on_leave unless @discard || (result == :exit_requested && @exit_deletes)
629
646
  result
@@ -18,7 +18,8 @@ module Samagotchi
18
18
  #
19
19
  # A client can also ask the worker to exit now (`/exit` in the attached
20
20
  # TUI, Bridge POST /exit): #hold_for_request applies the same rules without
21
- # the timeout and leaves out the asking client's own stream.
21
+ # the timeout and leaves out the asking client's own stream. A restart
22
+ # (POST /exit restart: true) has its own rules, #hold_for_restart.
22
23
  class WorkerIdleExit
23
24
  # @param engine [Engine] #turn_running?, #last_activity_at, #reminder_store
24
25
  # @param bridge [Bridge, nil] #open_streams, #last_client_activity_at; nil
@@ -26,14 +27,17 @@ module Samagotchi
26
27
  # @param timeout_minutes [Numeric, nil] 0 or nil: never exit
27
28
  # @param input_pending [#call] true while input files wait
28
29
  # @param awaiting_continue [#call] true while a continue offer waits
30
+ # @param running_tasks [#call] the background tasks this conversation
31
+ # started that still run (Tools::TaskRuntime.running_created_in)
29
32
  # @param clock [#call] monotonic seconds, the Engine's and Bridge's clock
30
33
  def initialize(engine:, bridge:, timeout_minutes:, input_pending:, awaiting_continue: -> { false },
31
- clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
34
+ running_tasks: -> { [] }, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
32
35
  @engine = engine
33
36
  @bridge = bridge
34
37
  @timeout = timeout_minutes.to_f * 60
35
38
  @input_pending = input_pending
36
39
  @awaiting_continue = awaiting_continue
40
+ @running_tasks = running_tasks
37
41
  @clock = clock
38
42
  @started_at = clock.call
39
43
  end
@@ -78,6 +82,31 @@ module Samagotchi
78
82
  nil
79
83
  end
80
84
 
85
+ # What keeps the worker from restarting now (a client asked: POST /exit
86
+ # restart: true), or nil. A restart drops what lives only in this
87
+ # process, so whatever would be lost or cut holds it: a question or
88
+ # approval waiting (a turn's), a /btw (or another anytime command)
89
+ # running, a delegate's approval relay this worker still answers for,
90
+ # background tasks this conversation started (their output and end are
91
+ # this worker's to report). Clients' streams don't hold: a web tab
92
+ # reconnects on the new worker's bridge_up, an attached terminal finds
93
+ # the new worker's Bridge.
94
+ # @return [Symbol, nil] :turn_running, :input_queued, :continue_offered,
95
+ # :question_pending, :reminders, :command_running, :relays_open or
96
+ # :background_tasks
97
+ def hold_for_restart
98
+ return :turn_running if @engine.turn_running?
99
+ return :input_queued if @input_pending.call
100
+ return :continue_offered if @awaiting_continue.call
101
+ return :question_pending if @engine.pending_question
102
+ return :reminders if @engine.reminder_store&.any?
103
+ return :command_running if @engine.anytime_running?
104
+ return :relays_open if @engine.relay_desk.active?
105
+ return :background_tasks if @running_tasks.call.any?
106
+
107
+ nil
108
+ end
109
+
81
110
  # @return [Float] seconds since the latest activity of any kind
82
111
  def idle_seconds
83
112
  last = [@started_at, @engine.last_activity_at, @bridge&.last_client_activity_at].compact.max
@@ -8,11 +8,12 @@ require_relative "atomic_file"
8
8
  module Samagotchi
9
9
  # A worker's bridge.json in its session directory: how clients reach its
10
10
  # Bridge (port, bind), when it started (a new worker behind the same
11
- # "worker" owner), the chi it runs (chi update) and the input-file format
12
- # it reads (SessionInbox::INPUT_FORMAT). The Bridge writes it on start
11
+ # "worker" owner), the chi it runs (chi update, the web's stale-worker
12
+ # badge), the input-file format it reads (SessionInbox::INPUT_FORMAT) and
13
+ # what else it can do (Bridge::FEATURES). The Bridge writes it on start
13
14
  # and removes it on stop; one a dead worker left stays until a client's
14
15
  # probe finds its port closed (.live with unlink: true).
15
- class WorkerSidecar < Data.define(:port, :bind, :session_id, :started_at, :version, :input_format)
16
+ class WorkerSidecar < Data.define(:port, :bind, :session_id, :started_at, :version, :input_format, :features)
16
17
  FILE = "bridge.json"
17
18
  # Seconds a liveness probe waits for the port to take a connect.
18
19
  PROBE_TIMEOUT = 0.2
@@ -27,7 +28,8 @@ module Samagotchi
27
28
 
28
29
  port = data["port"]
29
30
  new(port: port.respond_to?(:to_i) ? port.to_i : 0, bind: data["bind"], session_id: data["session_id"],
30
- started_at: data["started_at"], version: data["version"], input_format: data["input_format"])
31
+ started_at: data["started_at"], version: data["version"], input_format: data["input_format"],
32
+ features: Array(data["features"]).map(&:to_s))
31
33
  rescue JSON::ParserError, SystemCallError
32
34
  nil
33
35
  end
@@ -61,7 +63,7 @@ module Samagotchi
61
63
  # @return [Integer, nil] .live's port
62
64
  def self.live_port(session_dir, unlink:, host: "127.0.0.1") = live(session_dir, unlink: unlink, host: host)&.port
63
65
 
64
- def initialize(port:, bind: nil, session_id: nil, started_at: nil, version: nil, input_format: nil) = super
66
+ def initialize(port:, bind: nil, session_id: nil, started_at: nil, version: nil, input_format: nil, features: []) = super
65
67
 
66
68
  # Written whole or not at all; input_format only when it names one.
67
69
  def write(session_dir)
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.18.1
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -252,6 +252,7 @@ files:
252
252
  - lib/samagotchi/idle_scheduler.rb
253
253
  - lib/samagotchi/image_store.rb
254
254
  - lib/samagotchi/installed_gem.rb
255
+ - lib/samagotchi/installed_versions.rb
255
256
  - lib/samagotchi/iteration_limit.rb
256
257
  - lib/samagotchi/kernel_loop.rb
257
258
  - lib/samagotchi/launch_mode.rb
@@ -363,6 +364,7 @@ files:
363
364
  - lib/samagotchi/terminal_ui/status_row.rb
364
365
  - lib/samagotchi/terminal_ui/surface.rb
365
366
  - lib/samagotchi/terminal_ui/thinking_line.rb
367
+ - lib/samagotchi/terminal_ui/version_lines.rb
366
368
  - lib/samagotchi/text_diff.rb
367
369
  - lib/samagotchi/thinking.rb
368
370
  - lib/samagotchi/thinking_ticker.rb