samagotchi 0.4.0 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -1
- data/README.md +13 -2
- data/bin/chi +29 -39
- data/docs/cli.md +135 -73
- data/docs/configuration.md +15 -20
- data/docs/hooks.md +1 -1
- data/docs/memory.md +40 -0
- data/docs/plugins.md +50 -0
- data/docs/releasing.md +9 -6
- data/docs/sessions.md +3 -3
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -2
- data/lib/samagotchi/bridge/sse_writer.rb +0 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
- data/lib/samagotchi/bridge.rb +16 -11
- data/lib/samagotchi/bundles/skills/manifest.yml +10 -0
- data/lib/samagotchi/bundles/skills/plugin.rb +419 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -8
- data/lib/samagotchi/bundles/system/identity.md +5 -0
- data/lib/samagotchi/bundles/system/manifest.yml +5 -5
- data/lib/samagotchi/bundles/system/memory_guide.md +26 -0
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/client.rb +16 -20
- data/lib/samagotchi/config.rb +40 -100
- data/lib/samagotchi/engine.rb +50 -354
- data/lib/samagotchi/kernel_loop.rb +33 -44
- data/lib/samagotchi/live_versions.rb +7 -1
- data/lib/samagotchi/llm/errors.rb +17 -0
- data/lib/samagotchi/llm/http.rb +4 -18
- data/lib/samagotchi/llm/openai_chat.rb +17 -0
- data/lib/samagotchi/model_profile.rb +4 -10
- data/lib/samagotchi/note_command.rb +2 -1
- data/lib/samagotchi/reply_wait.rb +48 -4
- data/lib/samagotchi/self_report.rb +20 -2
- data/lib/samagotchi/send_command.rb +84 -6
- data/lib/samagotchi/session.rb +4 -2
- data/lib/samagotchi/session_manager.rb +18 -37
- data/lib/samagotchi/system_prompt.rb +403 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +120 -83
- data/lib/samagotchi/terminal_ui/attached_view.rb +27 -12
- data/lib/samagotchi/terminal_ui/event_renderer.rb +23 -7
- data/lib/samagotchi/terminal_ui/formatting.rb +32 -22
- data/lib/samagotchi/terminal_ui/input_support.rb +3 -4
- data/lib/samagotchi/terminal_ui/plain_surface.rb +13 -7
- data/lib/samagotchi/terminal_ui/status_row.rb +81 -0
- data/lib/samagotchi/terminal_ui/surface.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +95 -690
- data/lib/samagotchi/thinking.rb +11 -0
- data/lib/samagotchi/tool_activity.rb +52 -2
- data/lib/samagotchi/tool_runner.rb +3 -0
- data/lib/samagotchi/tools/execute.rb +3 -3
- data/lib/samagotchi/tools/output_guardrails.rb +8 -7
- data/lib/samagotchi/tools/read.rb +4 -4
- data/lib/samagotchi/update_command.rb +2 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +170 -35
- data/lib/samagotchi/web/lan.rb +99 -0
- data/lib/samagotchi/web/message_parts.rb +15 -11
- data/lib/samagotchi/web/public/activity.js +7 -0
- data/lib/samagotchi/web/public/app.js +99 -54
- data/lib/samagotchi/web/public/chat_view.js +5 -1
- data/lib/samagotchi/web/public/index.html +163 -17
- data/lib/samagotchi/web/public/model_pick.js +136 -0
- data/lib/samagotchi/web/public/model_picker.js +224 -0
- data/lib/samagotchi/web/public/notify.js +10 -0
- data/lib/samagotchi/web/public/stage_model.js +110 -0
- data/lib/samagotchi/web/public/stage_view.js +580 -0
- data/lib/samagotchi/web/public/timing.js +6 -2
- data/lib/samagotchi/web/public/turn_events.js +9 -5
- data/lib/samagotchi/web/public/turn_model.js +11 -3
- data/lib/samagotchi/web/public/turn_view.js +74 -19
- data/lib/samagotchi/web/qr.rb +40 -0
- data/lib/samagotchi/web/server.rb +101 -11
- data/lib/samagotchi/web/token.rb +97 -0
- metadata +27 -3
- data/lib/samagotchi/terminal_ui/legacy_surface.rb +0 -111
data/lib/samagotchi/config.rb
CHANGED
|
@@ -83,7 +83,7 @@ module Samagotchi
|
|
|
83
83
|
|
|
84
84
|
Entry.new(key: "session.retention_days", yaml_path: %w[session retention_days], type: :integer, default: 14, expose: %i[env config cli]),
|
|
85
85
|
Entry.new(key: "session.max_count", yaml_path: %w[session max_count], type: :integer, default: 500, expose: %i[env config cli]),
|
|
86
|
-
Entry.new(key: "session.keep_status", yaml_path: %w[session keep_status], type: :string, default: "
|
|
86
|
+
Entry.new(key: "session.keep_status", yaml_path: %w[session keep_status], type: :string, default: "", expose: %i[env config cli]),
|
|
87
87
|
Entry.new(key: "session.sweep_interval_hours", yaml_path: %w[session sweep_interval_hours], type: :integer, default: 24, expose: %i[env config cli]),
|
|
88
88
|
Entry.new(key: "session.idle_exit_minutes", yaml_path: %w[session idle_exit_minutes], type: :float, default: 30.0, expose: %i[env config cli]),
|
|
89
89
|
# Plain `chi` runs like `chi --shared` (bin/chi, LaunchMode); false, or --no-shared per run, keeps the plain REPL. No CLI flag: that would duplicate --shared.
|
|
@@ -108,9 +108,6 @@ module Samagotchi
|
|
|
108
108
|
Entry.new(key: "log.level", yaml_path: %w[log level], type: :enum, default: "info", expose: %i[env config cli], enum_values: %w[debug info warn error]),
|
|
109
109
|
|
|
110
110
|
Entry.new(key: "status.line", yaml_path: %w[status line], type: :string, default: "on", expose: %i[env config cli]),
|
|
111
|
-
Entry.new(key: "status.width_mode", yaml_path: %w[status width_mode], type: :string, default: "terminal_cap", expose: %i[env config cli]),
|
|
112
|
-
Entry.new(key: "status.max_width", yaml_path: %w[status max_width], type: :integer, default: 160, expose: %i[env config cli]),
|
|
113
|
-
Entry.new(key: "status.fixed_width", yaml_path: %w[status fixed_width], type: :integer, default: 120, expose: %i[env config cli]),
|
|
114
111
|
|
|
115
112
|
Entry.new(key: "context.status", yaml_path: %w[context status], type: :bool, default: true, expose: %i[env config cli]),
|
|
116
113
|
Entry.new(key: "context.window_tokens", yaml_path: %w[context window_tokens], type: :integer, default: nil, expose: %i[env config cli]),
|
|
@@ -118,8 +115,6 @@ module Samagotchi
|
|
|
118
115
|
Entry.new(key: "context.status_thresholds", yaml_path: %w[context status_thresholds],type: :string, default: "20,40,60,80", expose: %i[env config cli]),
|
|
119
116
|
Entry.new(key: "context.status_cadence", yaml_path: %w[context status_cadence], type: :integer, default: 0, expose: %i[env config cli]),
|
|
120
117
|
|
|
121
|
-
Entry.new(key: "thinking.ui", yaml_path: %w[thinking ui], type: :string, default: nil, expose: %i[env config cli]),
|
|
122
|
-
Entry.new(key: "thinking.render_interval", yaml_path: %w[thinking render_interval], type: :float, default: 0.08, expose: %i[env config cli]),
|
|
123
118
|
Entry.new(key: "thinking.turn_preamble", yaml_path: %w[thinking turn_preamble], type: :bool, default: true, expose: %i[env config cli]),
|
|
124
119
|
# How much models think: off|low|medium|high|default, parsed by Thinking
|
|
125
120
|
# (a :string, since YAML reads an unquoted off as false). The CLI flag
|
|
@@ -153,8 +148,8 @@ module Samagotchi
|
|
|
153
148
|
Entry.new(key: "web.port", yaml_path: %w[web port], type: :integer, default: 4567, expose: %i[env config cli]),
|
|
154
149
|
Entry.new(key: "web.host", yaml_path: %w[web host], type: :string, default: "127.0.0.1", expose: %i[env config cli]),
|
|
155
150
|
Entry.new(key: "web.markdown", yaml_path: %w[web markdown], type: :bool, default: false, expose: %i[env config cli]),
|
|
156
|
-
# The page
|
|
157
|
-
Entry.new(key: "web.
|
|
151
|
+
# The page's view of a turn: turn (one block of generations, the live one at the bottom), stage (the running turn pinned above the composer) or chat (a row of bubbles); ?view= overrides it per page load.
|
|
152
|
+
Entry.new(key: "web.view", yaml_path: %w[web view], type: :enum, default: "turn", expose: %i[env config cli], enum_values: %w[turn stage chat]),
|
|
158
153
|
# Quick replies next to Annotate in the page's selection bubble, "|"-separated (a YAML list works too); "" leaves only Annotate.
|
|
159
154
|
Entry.new(key: "web.annotate_presets", yaml_path: %w[web annotate_presets], type: :string, default: "Agreed|Could you please elaborate?", expose: %i[env config cli]),
|
|
160
155
|
|
|
@@ -179,12 +174,9 @@ module Samagotchi
|
|
|
179
174
|
}.freeze
|
|
180
175
|
# Section keys beyond the registry's: guardrails' YAML rules (Engine#guardrail_rules).
|
|
181
176
|
SECTION_EXTRA_KEYS = { "guardrails" => %w[rules disable].freeze }.freeze
|
|
182
|
-
# Removed settings that warn on their own (Engine.warn_removed_backend_setting).
|
|
183
|
-
REMOVED_KEYS = %w[backend].freeze
|
|
184
177
|
|
|
185
178
|
# Fast lookup maps
|
|
186
179
|
BY_KEY = ENTRIES.each_with_object({}) { |e, h| h[e.key] = e }.freeze
|
|
187
|
-
BY_ENV = ENTRIES.each_with_object({}) { |e, h| h[e.env_key] = e }.freeze
|
|
188
180
|
|
|
189
181
|
class << self
|
|
190
182
|
# The +candidates+ within edit distance 2 of +probe+, closest first
|
|
@@ -197,10 +189,6 @@ module Samagotchi
|
|
|
197
189
|
BY_KEY[key.to_s]
|
|
198
190
|
end
|
|
199
191
|
|
|
200
|
-
def find_by_env(env_key)
|
|
201
|
-
BY_ENV[env_key.to_s]
|
|
202
|
-
end
|
|
203
|
-
|
|
204
192
|
def all_entries
|
|
205
193
|
ENTRIES
|
|
206
194
|
end
|
|
@@ -316,26 +304,10 @@ module Samagotchi
|
|
|
316
304
|
end
|
|
317
305
|
|
|
318
306
|
# Lookup yaml_path in nested hash, accepting snake leaf, kebab alias and
|
|
319
|
-
# entry-specific yaml_aliases
|
|
320
|
-
# SAMAGOTCHI_DEFAULT_MODEL) for transition: the nested key wins when a
|
|
321
|
-
# file has both (ConfigFile.load_global_env! warns about that).
|
|
307
|
+
# entry-specific yaml_aliases.
|
|
322
308
|
def lookup_yaml(data, yaml_path)
|
|
323
309
|
entry = ENTRIES.find { |e| e.yaml_path == yaml_path }
|
|
324
|
-
|
|
325
|
-
return nested unless nested.nil?
|
|
326
|
-
|
|
327
|
-
lookup_legacy(data, entry)
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
# The legacy flat key's value for +entry+, or nil.
|
|
331
|
-
def lookup_legacy(data, entry)
|
|
332
|
-
return nil unless entry && data.is_a?(Hash)
|
|
333
|
-
|
|
334
|
-
key = entry.env_key
|
|
335
|
-
return data[key] if data.key?(key)
|
|
336
|
-
return data[key.to_sym] if data.key?(key.to_sym)
|
|
337
|
-
|
|
338
|
-
nil
|
|
310
|
+
lookup_nested(data, yaml_path, entry)
|
|
339
311
|
end
|
|
340
312
|
|
|
341
313
|
# The nested key's value for +yaml_path+, or nil.
|
|
@@ -374,21 +346,11 @@ module Samagotchi
|
|
|
374
346
|
end
|
|
375
347
|
end
|
|
376
348
|
|
|
377
|
-
#
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
snapshot(file_data: file_data, env: env, cli_overrides: cli_overrides)
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
# In-memory store for current process (populated after CLI parse)
|
|
385
|
-
def store
|
|
386
|
-
@store ||= load_snapshot
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
def reload!(path: nil, env: ENV, cli_overrides: {})
|
|
349
|
+
# Set this process's CLI settings (bin/chi, after it parsed the flags).
|
|
350
|
+
# Nothing is cached: #get resolves every call, reading config.yml
|
|
351
|
+
# again when it changed.
|
|
352
|
+
def reload!(cli_overrides: {})
|
|
390
353
|
@cli_overrides = cli_overrides.dup
|
|
391
|
-
@store = load_snapshot(path: path, env: env, cli_overrides: cli_overrides)
|
|
392
354
|
ensure
|
|
393
355
|
# The log resolves its file and level from here.
|
|
394
356
|
Samagotchi::Log.invalidate! if defined?(Samagotchi::Log)
|
|
@@ -398,6 +360,18 @@ module Samagotchi
|
|
|
398
360
|
@cli_overrides ||= {}
|
|
399
361
|
end
|
|
400
362
|
|
|
363
|
+
# This process's CLI settings as the env a spawned worker takes them
|
|
364
|
+
# from ({"SAMAGOTCHI_..." => "value"}): a worker gets no CLI args, and
|
|
365
|
+
# reads everything else from ENV and config.yml itself.
|
|
366
|
+
def cli_env
|
|
367
|
+
cli_overrides.each_with_object({}) do |(key, value), env|
|
|
368
|
+
entry = find_by_key(key) || ENTRIES.find { |e| e.cli_flag == key.to_s }
|
|
369
|
+
next unless entry&.env_exposed? && !value.nil?
|
|
370
|
+
|
|
371
|
+
env[entry.env_key] = value.to_s
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
401
375
|
def get(key)
|
|
402
376
|
get_with_origin(key).first
|
|
403
377
|
end
|
|
@@ -433,13 +407,13 @@ module Samagotchi
|
|
|
433
407
|
# code doesn't read (with a "did you mean" when a known key is close).
|
|
434
408
|
# Every config-exposed entry in ENTRIES is known as written; names under
|
|
435
409
|
# the maps are the user's, and host/model entries may hold the keys
|
|
436
|
-
# their readers take.
|
|
410
|
+
# their readers take.
|
|
437
411
|
def validate_yaml_sections(data)
|
|
438
412
|
return [] unless data.is_a?(Hash)
|
|
439
413
|
|
|
440
414
|
data.flat_map do |key, value|
|
|
441
415
|
key = key.to_s
|
|
442
|
-
if
|
|
416
|
+
if FREE_FORM_MAPS.include?(key)
|
|
443
417
|
[]
|
|
444
418
|
elsif MAP_ENTRY_KEYS.key?(key)
|
|
445
419
|
map_entry_problems(key, value)
|
|
@@ -474,7 +448,7 @@ module Samagotchi
|
|
|
474
448
|
def key_problem(dotted)
|
|
475
449
|
return nil if known_config_keys.include?(dotted)
|
|
476
450
|
|
|
477
|
-
entry = find_by_key(dotted)
|
|
451
|
+
entry = find_by_key(dotted)
|
|
478
452
|
if entry
|
|
479
453
|
where = entry.cli_exposed? ? "#{entry.env_key} or #{entry.cli_flag}" : entry.env_key
|
|
480
454
|
return "config: '#{dotted}' can't be set in config.yml; use #{where}"
|
|
@@ -482,7 +456,7 @@ module Samagotchi
|
|
|
482
456
|
|
|
483
457
|
canonical = ENTRIES.select(&:config_exposed?).map(&:key) + known_sections.to_a +
|
|
484
458
|
SECTION_EXTRA_KEYS.flat_map { |s, leaves| leaves.map { |l| "#{s}.#{l}" } } + FREE_FORM_MAPS + MAP_ENTRY_KEYS.keys
|
|
485
|
-
# A
|
|
459
|
+
# A flat SAMAGOTCHI_* key (the env name) compares by its nested form.
|
|
486
460
|
probe = dotted.start_with?("SAMAGOTCHI_") ? dotted.delete_prefix("SAMAGOTCHI_").downcase : dotted
|
|
487
461
|
unknown_key_message(dotted, probe, canonical)
|
|
488
462
|
end
|
|
@@ -533,7 +507,6 @@ module Samagotchi
|
|
|
533
507
|
XDG_CONFIG_HOME_ENV = "XDG_CONFIG_HOME"
|
|
534
508
|
CONFIG_DIR = "samagotchi"
|
|
535
509
|
CONFIG_FILE = "config.yml"
|
|
536
|
-
DEFAULT_MODEL_KEY = "SAMAGOTCHI_DEFAULT_MODEL"
|
|
537
510
|
MODEL_ALIASES_KEY = "model_aliases"
|
|
538
511
|
MODELS_KEY = "models"
|
|
539
512
|
|
|
@@ -641,47 +614,18 @@ module Samagotchi
|
|
|
641
614
|
end
|
|
642
615
|
end
|
|
643
616
|
|
|
644
|
-
#
|
|
645
|
-
#
|
|
646
|
-
#
|
|
647
|
-
#
|
|
648
|
-
#
|
|
649
|
-
def
|
|
617
|
+
# Check config.yml once at start (warnings for keys the code doesn't
|
|
618
|
+
# read) and clear the CLI settings. Nothing is copied into ENV: every
|
|
619
|
+
# Config.get reads the file again when it changed, so a long-lived
|
|
620
|
+
# process (chi web) and the workers it spawns see edits, and a file
|
|
621
|
+
# value reports origin :file. Returns true if the file existed.
|
|
622
|
+
def load!(env: ENV, path: global_path(env: env))
|
|
650
623
|
existed = File.file?(path)
|
|
651
624
|
raw = read_yaml(env: env, path: path) || {}
|
|
652
625
|
if existed
|
|
653
626
|
Samagotchi::Config.validate_yaml_sections(raw).each { |w| Log.warn(:config, "config_key", echo: "Warning: #{w}") }
|
|
654
|
-
# Warn on legacy UPPER keys; the nested key wins when both are set.
|
|
655
|
-
# Any other flat key is unknown to validate_yaml_sections.
|
|
656
|
-
raw.each_key do |k|
|
|
657
|
-
entry = Samagotchi::Config.find_by_env(k)
|
|
658
|
-
next unless entry&.config_exposed?
|
|
659
|
-
|
|
660
|
-
if !Samagotchi::Config.lookup_nested(raw, entry.yaml_path, entry).nil?
|
|
661
|
-
nested = entry.yaml_path.join(".")
|
|
662
|
-
Log.warn(:config, "legacy_key", echo: "Warning: config: both '#{k}' and '#{nested}' are set; using '#{nested}', remove the flat key", key: k.to_s)
|
|
663
|
-
else
|
|
664
|
-
Log.warn(:config, "legacy_key", echo: "Warning: config key '#{k}' is legacy UPPER — use '#{entry.yaml_path.join('.')}'", key: k.to_s)
|
|
665
|
-
end
|
|
666
|
-
end
|
|
667
|
-
end
|
|
668
|
-
# For process-wide access, prime the Config store (so Config.get works)
|
|
669
|
-
Samagotchi::Config.reload!(path: path, env: env, cli_overrides: {})
|
|
670
|
-
# Keep ENV in sync for any code still reading ENV directly (transition).
|
|
671
|
-
# Only for keys that were actually present in file (not defaults) to
|
|
672
|
-
# avoid polluting ENV with defaults.
|
|
673
|
-
Samagotchi::Config.all_entries.each do |entry|
|
|
674
|
-
next unless entry.env_exposed?
|
|
675
|
-
# The env's thinking.level outranks models:/hosts: levels, the file's doesn't (Thinking.resolve).
|
|
676
|
-
next if entry.key == "thinking.level"
|
|
677
|
-
# check if file actually contained this key (including legacy flat)
|
|
678
|
-
file_val = Samagotchi::Config.lookup_yaml(raw, entry.yaml_path)
|
|
679
|
-
file_val ||= raw[entry.env_key] if raw.key?(entry.env_key)
|
|
680
|
-
next if file_val.nil?
|
|
681
|
-
val = Samagotchi::Config.store[entry.key]
|
|
682
|
-
next if val.nil?
|
|
683
|
-
env[entry.env_key] = val.to_s unless env.key?(entry.env_key)
|
|
684
627
|
end
|
|
628
|
+
Samagotchi::Config.reload!(cli_overrides: {})
|
|
685
629
|
existed
|
|
686
630
|
end
|
|
687
631
|
|
|
@@ -815,15 +759,16 @@ module Samagotchi
|
|
|
815
759
|
end
|
|
816
760
|
end
|
|
817
761
|
|
|
818
|
-
# If no hosts defined, synthesize "default" from
|
|
762
|
+
# If no hosts defined, synthesize "default" from server.host/port/transport.
|
|
819
763
|
if normalized.empty?
|
|
820
|
-
|
|
764
|
+
opts = { file_data: data, env: env, cli_overrides: Samagotchi::Config.cli_overrides }
|
|
765
|
+
default_host = Samagotchi::Config.resolve("server.host", **opts).to_s.strip
|
|
821
766
|
default_host = "localhost" if default_host.empty?
|
|
822
|
-
default_port =
|
|
823
|
-
default_port = default_port.empty? ? 8080 : default_port.to_i
|
|
767
|
+
default_port = Samagotchi::Config.resolve("server.port", **opts).to_i
|
|
824
768
|
default_port = 8080 if default_port <= 0 || default_port > 65535
|
|
825
|
-
|
|
826
|
-
|
|
769
|
+
transport_sym = Samagotchi::Config.resolve_with_origin("server.transport", **opts).then do |value, origin|
|
|
770
|
+
origin == :default ? nil : value.to_sym
|
|
771
|
+
end
|
|
827
772
|
normalized["default"] = { name: "default", host: default_host, port: default_port, transport: transport_sym, original_name: "default" }
|
|
828
773
|
end
|
|
829
774
|
normalized
|
|
@@ -959,23 +904,18 @@ module Samagotchi
|
|
|
959
904
|
|
|
960
905
|
raw_data = read_yaml(env: env, path: path) || {}
|
|
961
906
|
|
|
962
|
-
# Migrate to new dotted nested form: default.model (Option A)
|
|
963
907
|
raw_data["default"] ||= {}
|
|
964
908
|
if raw_data["default"].is_a?(Hash)
|
|
965
909
|
raw_data["default"]["model"] = resolved
|
|
966
910
|
else
|
|
967
911
|
raw_data["default"] = { "model" => resolved }
|
|
968
912
|
end
|
|
969
|
-
# Remove legacy UPPER key if present
|
|
970
|
-
raw_data.delete(DEFAULT_MODEL_KEY)
|
|
971
913
|
FileUtils.mkdir_p(File.dirname(path))
|
|
972
914
|
tmp = "#{path}.tmp"
|
|
973
915
|
File.write(tmp, YAML.dump(raw_data))
|
|
974
916
|
File.rename(tmp, path)
|
|
975
917
|
clear_yaml_cache!(path)
|
|
976
|
-
|
|
977
|
-
# Also sync new Config store if loaded
|
|
978
|
-
Samagotchi::Config.reload!(path: path, env: env, cli_overrides: {})
|
|
918
|
+
Samagotchi::Config.reload!(cli_overrides: Samagotchi::Config.cli_overrides)
|
|
979
919
|
true
|
|
980
920
|
end
|
|
981
921
|
|