samagotchi 0.155.0 → 0.157.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 +57 -1
- data/bin/chi +2 -1
- data/docs/architecture.md +22 -13
- data/docs/bundles.md +3 -1
- data/docs/cli.md +9 -8
- data/docs/configuration.md +36 -4
- data/docs/guardrails.md +9 -5
- data/docs/hooks.md +11 -5
- data/docs/internals/anthropic.md +1 -1
- data/docs/internals/attached-context.md +3 -2
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +83 -6
- data/docs/internals/bundles.md +28 -18
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +641 -937
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +2 -2
- data/docs/internals/participants.md +3 -2
- data/docs/internals/prompt-caching.md +4 -1
- data/docs/internals/remote-sessions.md +8 -3
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +2 -2
- data/docs/memory.md +13 -2
- data/docs/plugins.md +35 -2
- data/docs/sessions.md +11 -11
- data/docs/sub-agent.md +17 -2
- data/docs/system-one.md +3 -2
- data/docs/testing.md +53 -2
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +10 -4
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +109 -9
- data/lib/samagotchi/bridge/turn_drawings.rb +265 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +18 -0
- data/lib/samagotchi/bundle_command/try/sandbox.rb +2 -2
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +5 -2
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/child_move.rb +13 -57
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +9 -23
- data/lib/samagotchi/config.rb +20 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_sources.rb +5 -4
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/context_judge.rb +19 -11
- data/lib/samagotchi/engine/messages.rb +1 -1
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +29 -11
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine/side_requests.rb +21 -11
- data/lib/samagotchi/engine/toolkit.rb +10 -9
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +7 -7
- data/lib/samagotchi/engine/verify.rb +1 -1
- data/lib/samagotchi/engine.rb +43 -12
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +6 -3
- data/lib/samagotchi/guardrails/targets.rb +24 -9
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +58 -46
- data/lib/samagotchi/kernel_loop.rb +53 -124
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/chat_loop.rb +40 -21
- data/lib/samagotchi/llm/native_backend.rb +1 -1
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +29 -67
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/memory_bundle/asset_installer.rb +71 -76
- data/lib/samagotchi/memory_bundle/builder.rb +2 -1
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_copy.rb +2 -1
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +8 -1
- data/lib/samagotchi/memory_bundle/installer.rb +185 -169
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +15 -1
- data/lib/samagotchi/memory_bundle/remerge.rb +25 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -6
- data/lib/samagotchi/memory_bundle/status.rb +14 -20
- data/lib/samagotchi/memory_bundle/system_bundle.rb +24 -6
- data/lib/samagotchi/memory_bundle/trash.rb +23 -8
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +7 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +2 -3
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/api.rb +41 -6
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/loader.rb +33 -10
- data/lib/samagotchi/plugin/sessions.rb +3 -6
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +27 -0
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +36 -37
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +370 -445
- data/lib/samagotchi/session_access/local.rb +12 -11
- data/lib/samagotchi/session_access/remote.rb +22 -6
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_field.rb +133 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +139 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +118 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +28 -1541
- data/lib/samagotchi/session_metrics.rb +8 -5
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui/join_replay.rb +86 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +5 -7
- data/lib/samagotchi/testing/install.rb +2 -1
- data/lib/samagotchi/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +10 -4
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +25 -5
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/delegate.rb +55 -45
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +5 -5
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/tools/registry.rb +28 -11
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +52 -51
- data/lib/samagotchi/tools/tool_surface.rb +22 -8
- data/lib/samagotchi/turn_context.rb +2 -1
- data/lib/samagotchi/turn_stages/context_judge.rb +38 -36
- data/lib/samagotchi/turn_stages/objectives.rb +56 -52
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +13 -17
- data/lib/samagotchi/turn_stages/verify_anchors.rb +9 -10
- data/lib/samagotchi/turn_stages/warmup.rb +14 -15
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +45 -28
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +3 -3
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +6 -6
- data/lib/samagotchi/web/session_control.rb +4 -4
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +67 -5
- data/lib/samagotchi/web/session_summary.rb +39 -28
- data/lib/samagotchi/worker.rb +83 -32
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_link.rb +3 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +32 -4
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -27,6 +27,7 @@ require_relative "thinking_notices"
|
|
|
27
27
|
require_relative "cancellation_controller"
|
|
28
28
|
require_relative "context_window"
|
|
29
29
|
require_relative "kernel_loop"
|
|
30
|
+
require_relative "reprefill_meter"
|
|
30
31
|
require_relative "tools/builtins"
|
|
31
32
|
require_relative "tools/task_runtime"
|
|
32
33
|
require_relative "tools/tool_index"
|
|
@@ -55,6 +56,7 @@ require_relative "context_judge"
|
|
|
55
56
|
require_relative "objectives_audit"
|
|
56
57
|
require_relative "owner_prompt"
|
|
57
58
|
require_relative "side_judgments"
|
|
59
|
+
require_relative "system_one/recorder"
|
|
58
60
|
require_relative "toolkit_suggest"
|
|
59
61
|
require_relative "turn_stages/context_judge"
|
|
60
62
|
require_relative "turn_stages/objectives"
|
|
@@ -228,7 +230,9 @@ module Samagotchi
|
|
|
228
230
|
@chat_backend_mutex = Mutex.new
|
|
229
231
|
@side_judgments = SideJudgments.new
|
|
230
232
|
@judgments = build_judgments
|
|
231
|
-
|
|
233
|
+
# Taken once: a running worker keeps the hosts it started with. A
|
|
234
|
+
# client (specs) goes on a registry of its own, never the shared one.
|
|
235
|
+
@host_registry = host_registry || (client ? HostRegistry.new : HostRegistry.current)
|
|
232
236
|
# The turn stages over the objectives (its audit, view and proposals)
|
|
233
237
|
# and the Verify/Revise anchors (TurnStages).
|
|
234
238
|
@objectives_stage = build_objectives_stage
|
|
@@ -451,6 +455,9 @@ module Samagotchi
|
|
|
451
455
|
self.guardrail_state_dir = Session.default_state_dir
|
|
452
456
|
# The turn-end warm-up; the kernel asks it for a slot pin per request.
|
|
453
457
|
@prompt_warmup = PromptWarmup.new
|
|
458
|
+
# One re-prefill baseline for both loops (the native kernel's and the
|
|
459
|
+
# chat loop's requests), reset by the warm-up (KernelLoop#warmed_up!).
|
|
460
|
+
@reprefill = ReprefillMeter.new
|
|
454
461
|
services = KernelLoop::Services.new(
|
|
455
462
|
hooks: @hooks, tools: @tools, reminder_store: @reminder_store, muted_memory_names: @muted_memory_names,
|
|
456
463
|
# ask_user_question blocks on the Engine's question flow (TUI/Web answer it).
|
|
@@ -466,8 +473,8 @@ module Samagotchi
|
|
|
466
473
|
)
|
|
467
474
|
# A given kernel (specs) gets the same services as the one built here.
|
|
468
475
|
@kernel = kernel || KernelLoop.new(client: @client, profile: @model_binding.given_profile,
|
|
469
|
-
model_name: effective_model_name, services: services)
|
|
470
|
-
@kernel.attach(services) if kernel
|
|
476
|
+
model_name: effective_model_name, services: services, reprefill: @reprefill)
|
|
477
|
+
@kernel.attach(services, reprefill: @reprefill) if kernel
|
|
471
478
|
sync_kernel_client!
|
|
472
479
|
sync_model_key!
|
|
473
480
|
# What a hook can do beyond reading its event (event[:notify],
|
|
@@ -713,7 +720,10 @@ module Samagotchi
|
|
|
713
720
|
# (its limit goes in last_turn). The end event names its turn
|
|
714
721
|
# (`turn_id:`, as :turn_started does) and, for a Verify turn,
|
|
715
722
|
# `verify: true` (its opening's stamp: a page watching it live offers
|
|
716
|
-
# no Revise on its answer, verdict or not, as a reload does).
|
|
723
|
+
# no Revise on its answer, verdict or not, as a reload does). When the
|
|
724
|
+
# turn keeps messages, the event also names its own range in them,
|
|
725
|
+
# `kept[kept_from...kept_to]` (display-side: Bridge::TurnDrawings
|
|
726
|
+
# reads it; no message gains a key).
|
|
717
727
|
def end_turn(turn, outcome, save: false, exhausted: false)
|
|
718
728
|
seconds = turn.elapsed
|
|
719
729
|
turn.outcome = outcome
|
|
@@ -722,12 +732,13 @@ module Samagotchi
|
|
|
722
732
|
event[:turn_id] = turn.id
|
|
723
733
|
event[:verify] = true if turn.verify
|
|
724
734
|
revises = @verify_anchors.stamp_opening(kept, turn, outcome)
|
|
735
|
+
kept_range!(event, turn, kept)
|
|
725
736
|
replace_session_messages(turn.session, kept) if kept
|
|
726
737
|
verdict = @verify_anchors.record_verdict(turn, outcome)
|
|
727
738
|
event[:verdict] = verdict if verdict
|
|
728
739
|
event[:revises] = revises.to_h if revises
|
|
729
740
|
emit_working_set(turn.on_event, turn.session, turn.session.messages)
|
|
730
|
-
|
|
741
|
+
announce_objectives(turn.session)
|
|
731
742
|
turn.session.status = Session::STATUS_IDLE
|
|
732
743
|
record_last_turn(turn.session, outcome, seconds, turn.origin, event, limit: exhausted ? turn.limit : nil)
|
|
733
744
|
emit_event(turn.on_event, turn.tag(event))
|
|
@@ -739,6 +750,20 @@ module Samagotchi
|
|
|
739
750
|
end
|
|
740
751
|
private :end_turn
|
|
741
752
|
|
|
753
|
+
# The end event's kept_from/kept_to (#end_turn): only when the turn
|
|
754
|
+
# keeps messages and its start is known (#turn_messages ran). An ending
|
|
755
|
+
# that replaces an earlier turn's note at the history's tail
|
|
756
|
+
# (TurnNote.replace_trailing on a Continue stopped before its first step)
|
|
757
|
+
# leaves only system messages and its own note after it, so the range
|
|
758
|
+
# still starts at or before the turn's note and draws nothing else.
|
|
759
|
+
def kept_range!(event, turn, kept)
|
|
760
|
+
return unless kept && turn.history_size
|
|
761
|
+
|
|
762
|
+
event[:kept_from] = turn.history_size
|
|
763
|
+
event[:kept_to] = kept.size
|
|
764
|
+
end
|
|
765
|
+
private :kept_range!
|
|
766
|
+
|
|
742
767
|
# What a notice may stand in for (hook_notify's fallback_for:).
|
|
743
768
|
NOTICE_FALLBACKS = HookRoutes::NOTICE_FALLBACKS
|
|
744
769
|
|
|
@@ -801,8 +826,11 @@ module Samagotchi
|
|
|
801
826
|
# +wall+: the wall time it started (MessageTime.now, once): the
|
|
802
827
|
# :turn_started event's at and its user message's (and reminders') stamp.
|
|
803
828
|
# +verify+, +revises+: a Verify turn's claim, a Revise turn's draft.
|
|
829
|
+
# +history_size+: turn.messages' size under the system head, before
|
|
830
|
+
# the reminders and the prompt (#turn_messages): where the turn's own
|
|
831
|
+
# messages start in what it keeps (the end event's kept_from).
|
|
804
832
|
Turn = Struct.new(:context, :prompt, :continue, :on_event, :started_at, :messages, :ended, :prepared_settings,
|
|
805
|
-
:limit, :resumed, :resume_from, :wall, :verify, :revises, :outcome) do
|
|
833
|
+
:limit, :resumed, :resume_from, :wall, :verify, :revises, :outcome, :history_size) do
|
|
806
834
|
# The turn's id (:turn_started's turn_id, its metrics record's id and
|
|
807
835
|
# its saved prompt's), session, origin and controller: its context's.
|
|
808
836
|
def id = context.id
|
|
@@ -911,7 +939,7 @@ module Samagotchi
|
|
|
911
939
|
pending_input: pending_input)
|
|
912
940
|
publish_used_memories(session, on_event)
|
|
913
941
|
complete_turn(turn, result)
|
|
914
|
-
@turn_warmup.after(turn, result)
|
|
942
|
+
@turn_warmup.after(turn, result, next_turn_waiting: next_turn_waiting)
|
|
915
943
|
result
|
|
916
944
|
rescue Interrupt => e
|
|
917
945
|
# After the end event (in the after_turn/session_end hooks) the
|
|
@@ -941,7 +969,7 @@ module Samagotchi
|
|
|
941
969
|
@context_judge.show_cards_after_turn
|
|
942
970
|
# Only a completed turn shows its proposal cards; any other's are
|
|
943
971
|
# dropped, so the next turn does not show them either.
|
|
944
|
-
|
|
972
|
+
show_objective_proposals(turn)
|
|
945
973
|
end
|
|
946
974
|
end
|
|
947
975
|
|
|
@@ -954,7 +982,7 @@ module Samagotchi
|
|
|
954
982
|
# A participant's Verify fresh child: every turn is theirs (their
|
|
955
983
|
# author, boundary and cap), whoever started it.
|
|
956
984
|
origin = session.verify_fresh.origin_for(origin) if session.respond_to?(:verify_fresh) && session.verify_fresh
|
|
957
|
-
verify = @verify_anchors.verified(session, VerifyAnchor.from(verify))
|
|
985
|
+
verify = @verify_anchors.verified(session, VerifyAnchor.from(verify), state_dir: session_state_dir)
|
|
958
986
|
# Track the active session for recap and status snapshot.
|
|
959
987
|
@session = session
|
|
960
988
|
# status is turn state: running now, idle again before the turn's end
|
|
@@ -1044,7 +1072,7 @@ module Samagotchi
|
|
|
1044
1072
|
# On mode's ask starts here too; the turn joins it before
|
|
1045
1073
|
# apply_toolkit! below, so its latency hides behind the probes and
|
|
1046
1074
|
# the init wait.
|
|
1047
|
-
@toolkit_suggestion.start(turn)
|
|
1075
|
+
@toolkit_suggestion.start(turn, recorder: system_one_recorder)
|
|
1048
1076
|
|
|
1049
1077
|
# The window and the profile probes run after run_turn's probe-cancel
|
|
1050
1078
|
# swap, so a Stop cuts them; a failure ends the turn as turn_failed.
|
|
@@ -1081,8 +1109,8 @@ module Samagotchi
|
|
|
1081
1109
|
@toolkit_suggestion.settle(turn)
|
|
1082
1110
|
# The candidate judge: its asks run beside the turn; on mode's
|
|
1083
1111
|
# attaches reach the model at the next between-turn delivery.
|
|
1084
|
-
@context_judge.start(turn)
|
|
1085
|
-
@objectives_stage.incoming(turn)
|
|
1112
|
+
@context_judge.start(turn, recorder: system_one_recorder)
|
|
1113
|
+
@objectives_stage.incoming(turn, recorder: system_one_recorder)
|
|
1086
1114
|
apply_toolkit!(session, turn.on_event)
|
|
1087
1115
|
refresh_participants!(session)
|
|
1088
1116
|
# A Stop during the waits above: the turn ends here,
|
|
@@ -1119,6 +1147,9 @@ module Samagotchi
|
|
|
1119
1147
|
# Built once per Engine (and again after a model switch) so the prompt
|
|
1120
1148
|
# prefix, and the model server's KV cache for it, stay stable.
|
|
1121
1149
|
turn.messages = ContextNote.with_system_head(turn.messages, { role: "system", content: system_prompt })
|
|
1150
|
+
# Where the turn's own messages start (the end event's kept_from):
|
|
1151
|
+
# after the head this may have added, before the reminders and prompt.
|
|
1152
|
+
turn.history_size = turn.messages.size
|
|
1122
1153
|
# Explicit --memory preloads are now known after system prompt build.
|
|
1123
1154
|
@used_memories.add(activated_memory_names).absorb(session)
|
|
1124
1155
|
|
data/lib/samagotchi/events.rb
CHANGED
|
@@ -13,8 +13,9 @@ module Samagotchi
|
|
|
13
13
|
# (#kept_turn?).
|
|
14
14
|
TURN_KEPT = %i[turn_completed turn_canceled].freeze
|
|
15
15
|
# The kinds of a running turn's parts (Bridge::TurnAccumulator), which a
|
|
16
|
-
# joining UI replays as live events
|
|
17
|
-
|
|
16
|
+
# joining UI replays as live events (a card part only in a finished
|
|
17
|
+
# turn's drawing: a join takes its cards from CardStore).
|
|
18
|
+
PART_KINDS = %w[generation thinking text tool input steer reminder notice card].freeze
|
|
18
19
|
# Types no Engine emits that a UI handles: the Bridge's stream frames
|
|
19
20
|
# and the web's one synthetic replay event (a prompt merged into the
|
|
20
21
|
# turn, turn_events.js snapshotEvents).
|
|
@@ -49,12 +49,15 @@ module Samagotchi
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Load the installed bundles' plugins into +registries+; one that fails
|
|
52
|
-
# goes to +failures+,
|
|
53
|
-
#
|
|
52
|
+
# goes to +failures+, a required one (a guardrail plugin) to the hook
|
|
53
|
+
# failures, where it denies every tool call, and the rest still load.
|
|
54
|
+
# The block runs after them, still loading: what it shows is held with
|
|
55
|
+
# theirs.
|
|
54
56
|
def load_plugins(registries, failures:)
|
|
55
57
|
@held_events = []
|
|
56
58
|
@loading = true
|
|
57
|
-
Plugin::Loader.load_installed(registries, failures: failures, settings: bundle_settings)
|
|
59
|
+
Plugin::Loader.load_installed(registries, failures: failures, guardrail_failures: @hook_failures, settings: bundle_settings)
|
|
60
|
+
.tap { yield if block_given? }
|
|
58
61
|
ensure
|
|
59
62
|
@loading = false
|
|
60
63
|
end
|
|
@@ -113,8 +113,10 @@ module Samagotchi
|
|
|
113
113
|
# @param model_name [String, nil] the bare model name, for a rule's models:
|
|
114
114
|
# @param registry [Tools::Registry, nil] the session's tools: a plugin
|
|
115
115
|
# tool's entry says what it acts on (its targets:)
|
|
116
|
-
# @param surface [Tools::ToolSurface, nil] the turn's:
|
|
117
|
-
#
|
|
116
|
+
# @param surface [Tools::ToolSurface, nil] the turn's: the entry is
|
|
117
|
+
# looked up in it (.entry_of) and call_tool's targets name what it
|
|
118
|
+
# reaches (nil: the registry's entry; call_tool's then raises
|
|
119
|
+
# Tools::MissingSurface)
|
|
118
120
|
def self.for(call, context, model_key: nil, model_name: nil, registry: nil, surface: nil)
|
|
119
121
|
tool = call[:name].to_s
|
|
120
122
|
base = context.cwd
|
|
@@ -143,7 +145,7 @@ module Samagotchi
|
|
|
143
145
|
given_cwd = given[:cwd].to_s.strip
|
|
144
146
|
cwd = File.expand_path(given_cwd, base) unless given_cwd.empty?
|
|
145
147
|
paths.concat(given[:paths].flat_map { |path| absolutes(path, cwd) })
|
|
146
|
-
entry = registry
|
|
148
|
+
entry = entry_of(tool, registry, surface)
|
|
147
149
|
args = given[:args] || call[:args] if entry && !entry.core? && call[:args].is_a?(Hash)
|
|
148
150
|
acts_as = given[:acts_as]
|
|
149
151
|
label = given[:label]
|
|
@@ -164,9 +166,12 @@ module Samagotchi
|
|
|
164
166
|
# logged). args: counts when it is a Hash; acts_as: is dropped (and
|
|
165
167
|
# logged) when it names a core tool or another bundle's tool, so a
|
|
166
168
|
# call can't pass for one and match (or reuse the approvals of) its
|
|
167
|
-
# rules. label: counts when it is one short line (.label_of).
|
|
169
|
+
# rules. label: counts when it is one short line (.label_of). A
|
|
170
|
+
# missing surface (Tools::MissingSurface) is raised, never taken as
|
|
171
|
+
# nothing: call_tool's acts_as would go missing, and with it the
|
|
172
|
+
# mcp_* rules on the call (fail-open).
|
|
168
173
|
def self.plugin_targets(call, registry, surface = nil)
|
|
169
|
-
entry =
|
|
174
|
+
entry = entry_of(call[:name], registry, surface)
|
|
170
175
|
none = { command: nil, paths: [], cwd: nil, acts_as: nil, args: nil, label: nil }
|
|
171
176
|
return none unless entry && !entry.core? && entry.targets
|
|
172
177
|
|
|
@@ -176,8 +181,10 @@ module Samagotchi
|
|
|
176
181
|
given = given.transform_keys(&:to_sym)
|
|
177
182
|
command = given[:command].to_s
|
|
178
183
|
{ command: command.empty? ? nil : command, paths: Array(given[:paths]).map(&:to_s), cwd: given[:cwd],
|
|
179
|
-
acts_as: acts_as_of(given[:acts_as], entry, registry), args: given[:args].is_a?(Hash) ? given[:args] : nil,
|
|
184
|
+
acts_as: acts_as_of(given[:acts_as], entry, registry, surface), args: given[:args].is_a?(Hash) ? given[:args] : nil,
|
|
180
185
|
label: label_of(given[:label], entry) }
|
|
186
|
+
rescue Tools::MissingSurface
|
|
187
|
+
raise
|
|
181
188
|
rescue StandardError => e
|
|
182
189
|
Log.warn(:plugins, "plugin_targets_failed", tool: call[:name].to_s, error: e.class.name,
|
|
183
190
|
msg: "#{call[:name]} targets failed: #{e.message}")
|
|
@@ -192,24 +199,32 @@ module Samagotchi
|
|
|
192
199
|
# name, ToolIndex#claimed), so the registry is the wrong owner test,
|
|
193
200
|
# and a bundle tool sharing an MCP tool's name would otherwise take
|
|
194
201
|
# call_tool calls of that tool out of its mcp_* rules.
|
|
195
|
-
def self.acts_as_of(name, entry, registry)
|
|
202
|
+
def self.acts_as_of(name, entry, registry, surface = nil)
|
|
196
203
|
name = name.to_s.strip
|
|
197
204
|
return nil if name.empty? || name == entry.name
|
|
198
205
|
|
|
199
|
-
other = registry
|
|
206
|
+
other = entry_of(name, registry, surface)
|
|
200
207
|
whose = if CORE_TOOLS.include?(name) || other&.core? then "a core tool"
|
|
201
208
|
elsif other && other.source != entry.source && entry.source != Tools::ToolIndex::SOURCE
|
|
202
209
|
"bundle #{other.source}'s tool"
|
|
203
210
|
end
|
|
204
211
|
return name unless whose
|
|
205
212
|
|
|
206
|
-
if first_drop?(registry, entry.name, name)
|
|
213
|
+
if first_drop?(registry || surface, entry.name, name)
|
|
207
214
|
Log.warn(:plugins, "plugin_acts_as_dropped", tool: entry.name, acts_as: name,
|
|
208
215
|
msg: "#{entry.name} targets: acts_as #{name} dropped: it is #{whose}")
|
|
209
216
|
end
|
|
210
217
|
nil
|
|
211
218
|
end
|
|
212
219
|
|
|
220
|
+
# The tool +name+ names: the turn's entry (+surface+'s registered),
|
|
221
|
+
# else +registry+'s (a gate without a turn's surface); nil for none.
|
|
222
|
+
# @return [Tools::Registry::Entry, nil]
|
|
223
|
+
def self.entry_of(name, registry, surface)
|
|
224
|
+
name = name.to_s
|
|
225
|
+
surface ? surface.registered[name] : registry&.[](name)
|
|
226
|
+
end
|
|
227
|
+
|
|
213
228
|
# The drops logged, per registry (a session's): each tool's acts_as
|
|
214
229
|
# once, not at every call (targets run several times a call).
|
|
215
230
|
DROPS_LOGGED = ObjectSpace::WeakKeyMap.new
|
|
@@ -190,7 +190,7 @@ module Samagotchi
|
|
|
190
190
|
threshold = threshold(options[:threshold])
|
|
191
191
|
return usage_error(threshold) if threshold.is_a?(String)
|
|
192
192
|
|
|
193
|
-
client = @client || SystemOne::Client.from_config(host_registry: HostRegistry.
|
|
193
|
+
client = @client || SystemOne::Client.from_config(host_registry: HostRegistry.current, mode: Config.get("guardrails.system_one.mode"), model: options[:model])
|
|
194
194
|
if client.nil?
|
|
195
195
|
error_line("chi guardrails replay: no System 1 client — set guardrails.system_one.mode to audit or on " \
|
|
196
196
|
"and system_one.host in config.yml")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require_relative "config"
|
|
5
|
+
|
|
6
|
+
module Samagotchi
|
|
7
|
+
# What a host's address says about it, without asking it: whether it is
|
|
8
|
+
# remote (a provider on the network rather than a local server), and the
|
|
9
|
+
# configured first-token limit. A leaf: HostRegistry::HostEntry and
|
|
10
|
+
# BootstrapCommand read it, it reads only Config.
|
|
11
|
+
module HostAddress
|
|
12
|
+
# Loopback, private (RFC 1918, IPv6 unique local) and link-local nets.
|
|
13
|
+
LOCAL_NETS = %w[127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 100.64.0.0/10
|
|
14
|
+
::1/128 fc00::/7 fe80::/10].map { |net| IPAddr.new(net) }.freeze
|
|
15
|
+
|
|
16
|
+
# Name suffixes that stay on a home or office network: mDNS, the
|
|
17
|
+
# special-use and customary private names, router defaults (fritz.box)
|
|
18
|
+
# and Tailscale's MagicDNS. Compared on whole labels.
|
|
19
|
+
LOCAL_NAME_SUFFIXES = %w[localhost local lan home home.arpa internal intranet localdomain private corp test
|
|
20
|
+
box ts.net].freeze
|
|
21
|
+
|
|
22
|
+
# Whether an address is remote: https, an http IP address outside the
|
|
23
|
+
# local nets (LOCAL_NETS), or an http name with a dot whose suffix isn't
|
|
24
|
+
# a local one (LOCAL_NAME_SUFFIXES): `gpu.example.com` is remote, while
|
|
25
|
+
# `box`, `mac.local`, `gpu.lan` and `pc.tail1234.ts.net` are local. chi
|
|
26
|
+
# doesn't look names up; hosts.<name>.remote overrides either way.
|
|
27
|
+
def self.remote_address?(scheme, host)
|
|
28
|
+
return true if scheme.to_s == "https"
|
|
29
|
+
|
|
30
|
+
address = IPAddr.new(host.to_s.delete_prefix("[").delete_suffix("]"))
|
|
31
|
+
address = address.native if address.ipv4_mapped?
|
|
32
|
+
LOCAL_NETS.none? { |net| net.family == address.family && net.include?(address) }
|
|
33
|
+
rescue IPAddr::Error
|
|
34
|
+
remote_name?(host)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.remote_name?(host)
|
|
38
|
+
name = host.to_s.downcase.delete_suffix(".")
|
|
39
|
+
return false unless name.include?(".")
|
|
40
|
+
|
|
41
|
+
LOCAL_NAME_SUFFIXES.none? { |suffix| name == suffix || name.end_with?(".#{suffix}") }
|
|
42
|
+
end
|
|
43
|
+
private_class_method :remote_name?
|
|
44
|
+
|
|
45
|
+
# server.first_token_timeout, or nil when unset or unreadable.
|
|
46
|
+
def self.configured_first_token_timeout
|
|
47
|
+
Config.get("server.first_token_timeout")
|
|
48
|
+
rescue StandardError # degrade-ok: no configured limit; the host's own or the remote default applies
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -8,7 +8,7 @@ require_relative "llm/dialect"
|
|
|
8
8
|
require_relative "reasoning_history"
|
|
9
9
|
|
|
10
10
|
module Samagotchi
|
|
11
|
-
# One config.yml hosts: entry, parsed and checked (
|
|
11
|
+
# One config.yml hosts: entry, parsed and checked (HostConfig.all
|
|
12
12
|
# holds them by lowercased name; HostRegistry builds its HostEntry from
|
|
13
13
|
# one).
|
|
14
14
|
#
|
|
@@ -28,6 +28,77 @@ module Samagotchi
|
|
|
28
28
|
super(**members.to_h { |m| [m, nil] }, models: {}, **fields)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# config.yml's hosts: by lowercased name; an entry that is disabled or
|
|
32
|
+
# invalid (warned) is left out. No entry left: "default", from
|
|
33
|
+
# server.host/port.
|
|
34
|
+
# @return [Hash{String => HostConfig}]
|
|
35
|
+
def self.all(env: ENV, path: ConfigFile.global_path(env: env))
|
|
36
|
+
data = ConfigFile.read_yaml(env: env, path: path)
|
|
37
|
+
raw_hosts = data[ConfigFile::HOSTS_KEY] if data.is_a?(Hash)
|
|
38
|
+
|
|
39
|
+
normalized = {}
|
|
40
|
+
if raw_hosts.is_a?(Hash)
|
|
41
|
+
raw_hosts.each do |raw_name, raw_cfg|
|
|
42
|
+
host = parse(raw_name, raw_cfg)
|
|
43
|
+
normalized[host.name] = host if host
|
|
44
|
+
end
|
|
45
|
+
warn_duplicate_models(normalized)
|
|
46
|
+
end
|
|
47
|
+
normalized["default"] = default_host(data, env) if normalized.empty?
|
|
48
|
+
normalized
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
Log.error(:config, "hosts_config_failed", error: e.class.name, message: e.message)
|
|
51
|
+
{}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The names (lowercased) of the hosts entries with enabled: false, which
|
|
55
|
+
# .all leaves out, from config.yml's hosts:.
|
|
56
|
+
# @return [Array<String>]
|
|
57
|
+
def self.disabled_names(env: ENV, path: ConfigFile.global_path(env: env))
|
|
58
|
+
data = ConfigFile.read_yaml(env: env, path: path)
|
|
59
|
+
raw = data[ConfigFile::HOSTS_KEY] if data.is_a?(Hash)
|
|
60
|
+
return [] unless raw.is_a?(Hash)
|
|
61
|
+
|
|
62
|
+
raw.filter_map { |name, cfg| name.to_s.strip.downcase if cfg.is_a?(Hash) && disabled?(cfg) }
|
|
63
|
+
rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
|
|
64
|
+
[]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# +raw+ parsed against config.yml's hosts (or +hosts+) and aliases.
|
|
68
|
+
# @return [ModelRef]
|
|
69
|
+
def self.model_ref(raw, env: ENV, path: ConfigFile.global_path(env: env), hosts: nil)
|
|
70
|
+
ModelRef.parse(raw, hosts: hosts || all(env: env, path: path), aliases: ConfigFile.model_aliases(env: env, path: path))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The host chi uses when hosts: names none: server.host/port.
|
|
74
|
+
def self.default_host(data, env)
|
|
75
|
+
opts = { file_data: data, env: env, cli_overrides: Config.cli_overrides }
|
|
76
|
+
host = Config.resolve("server.host", **opts).to_s.strip
|
|
77
|
+
host = "localhost" if host.empty?
|
|
78
|
+
port = Config.resolve("server.port", **opts).to_i
|
|
79
|
+
port = 8080 if port <= 0 || port > 65_535
|
|
80
|
+
new(name: "default", host: host, port: port)
|
|
81
|
+
end
|
|
82
|
+
private_class_method :default_host
|
|
83
|
+
|
|
84
|
+
# Two hosts declaring one id under hosts.<name>.models: a bare id goes
|
|
85
|
+
# where HostRegistry#host_for_model sends it (the default host when it
|
|
86
|
+
# declares the id, else the first in hosts: order); say so once.
|
|
87
|
+
def self.warn_duplicate_models(hosts)
|
|
88
|
+
declared = Hash.new { |h, k| h[k] = [] }
|
|
89
|
+
hosts.each { |name, cfg| cfg.models.each { |down, model| declared[down] << [name, model.id] } }
|
|
90
|
+
declared.each_value do |owners|
|
|
91
|
+
next if owners.size < 2
|
|
92
|
+
|
|
93
|
+
names = owners.map(&:first)
|
|
94
|
+
id = owners.first.last
|
|
95
|
+
goes = names.include?("default") ? "default" : "#{names.first} unless the default host lists it"
|
|
96
|
+
ConfigFile.warn_once "Warning: #{names.map { |n| "hosts.#{n}.models" }.join(" and ")} #{names.size == 2 ? "both" : "all"} " \
|
|
97
|
+
"declare #{id}; a bare #{id} goes to #{goes}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
private_class_method :warn_duplicate_models
|
|
101
|
+
|
|
31
102
|
# A hosts: entry as written in config.yml,
|
|
32
103
|
# or nil when it is disabled (enabled: false) or invalid. An invalid
|
|
33
104
|
# one warns once, naming the entry, the way each reader of a setting
|
|
@@ -67,12 +67,8 @@ module Samagotchi
|
|
|
67
67
|
served == "any" || served.any? { |id| id.casecmp?(name.to_s.strip) }
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
def self.warn_once(message)
|
|
73
|
-
require_relative "config"
|
|
74
|
-
ConfigFile.warn_once(message)
|
|
75
|
-
end
|
|
70
|
+
# Config comes with model_price.
|
|
71
|
+
def self.warn_once(message) = ConfigFile.warn_once(message)
|
|
76
72
|
private_class_method :warn_once
|
|
77
73
|
|
|
78
74
|
# One host's rows as the listings show them, declared ids first, then
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "ipaddr"
|
|
4
3
|
require_relative "config"
|
|
4
|
+
require_relative "host_address"
|
|
5
5
|
require_relative "log"
|
|
6
6
|
require_relative "model_ref"
|
|
7
7
|
require_relative "client"
|
|
@@ -88,11 +88,11 @@ module Samagotchi
|
|
|
88
88
|
def dialect_profile = LLM::Dialect.for(dialect)
|
|
89
89
|
|
|
90
90
|
# A provider on the network rather than a local server: hosts.<name>.remote
|
|
91
|
-
# when set, else by its address (
|
|
91
|
+
# when set, else by its address (HostAddress.remote_address?). An API
|
|
92
92
|
# key doesn't decide: a llama.cpp on the LAN can have one. A remote
|
|
93
93
|
# host's model list is cached longer, it gets a first-token limit, and
|
|
94
94
|
# it isn't asked for llama.cpp's /props.
|
|
95
|
-
def remote? = remote.nil? ?
|
|
95
|
+
def remote? = remote.nil? ? HostAddress.remote_address?(scheme, host) : remote
|
|
96
96
|
|
|
97
97
|
def models_ttl = remote? ? REMOTE_CACHE_TTL_SECONDS : CACHE_TTL_SECONDS
|
|
98
98
|
|
|
@@ -102,7 +102,7 @@ module Samagotchi
|
|
|
102
102
|
# and read_timeout catches a dead one). 0 turns it off.
|
|
103
103
|
def first_token_limit
|
|
104
104
|
seconds = first_token_timeout
|
|
105
|
-
seconds =
|
|
105
|
+
seconds = HostAddress.configured_first_token_timeout if seconds.nil?
|
|
106
106
|
seconds = remote? ? REMOTE_FIRST_TOKEN_TIMEOUT : nil if seconds.nil?
|
|
107
107
|
seconds&.positive? ? seconds : nil
|
|
108
108
|
end
|
|
@@ -115,18 +115,66 @@ module Samagotchi
|
|
|
115
115
|
def openai_base_url = entry.openai_base_url
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
+
# Raised when a client override is set on the shared registry (.current):
|
|
119
|
+
# every command and chi web's routes would then send through it.
|
|
120
|
+
class Shared < StandardError
|
|
121
|
+
def initialize(msg = "HostRegistry.current is shared: set a client override on a registry of your own") = super
|
|
122
|
+
end
|
|
123
|
+
|
|
118
124
|
# A client that every target uses instead of its host's own (specs inject
|
|
119
125
|
# a stub this way; the Engine/TUI `client:` keyword sets it).
|
|
120
|
-
|
|
126
|
+
attr_reader :client_override
|
|
127
|
+
|
|
128
|
+
# @raise [Shared] on the shared registry (.current)
|
|
129
|
+
def client_override=(client)
|
|
130
|
+
raise Shared if @shared
|
|
131
|
+
|
|
132
|
+
@client_override = client
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
@current_mutex = Mutex.new
|
|
136
|
+
|
|
137
|
+
# The registry this process's commands and chi web share: one per
|
|
138
|
+
# process, built again when config.yml's hosts: no longer equal the ones
|
|
139
|
+
# it was built from (HostConfig.all, compared as read: the YAML read is
|
|
140
|
+
# mtime-cached), so a host added while chi web runs is listed on the next
|
|
141
|
+
# read, and its model lists, index and adapters are kept while hosts:
|
|
142
|
+
# stay. It doesn't rebuild on server.first_token_timeout or the
|
|
143
|
+
# server.host/port fallback entry: those are read at construction. It
|
|
144
|
+
# refuses a client override (Shared). An Engine and the TerminalUI take
|
|
145
|
+
# it once, at their start: a running worker keeps the hosts it started
|
|
146
|
+
# with.
|
|
147
|
+
# For any env but the process ENV (chi self's spec envs): a fresh
|
|
148
|
+
# registry on that env, never kept.
|
|
149
|
+
# @return [HostRegistry]
|
|
150
|
+
def self.current(env: ENV)
|
|
151
|
+
return new(env: env) unless env.equal?(ENV)
|
|
152
|
+
|
|
153
|
+
@current_mutex.synchronize do
|
|
154
|
+
hosts = HostConfig.all
|
|
155
|
+
if @current.nil? || hosts != @current_hosts
|
|
156
|
+
@current = new(hosts_config: hosts, shared: true)
|
|
157
|
+
@current_hosts = hosts
|
|
158
|
+
end
|
|
159
|
+
@current
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Forget the shared registry (specs: one per example).
|
|
164
|
+
def self.reset_current!
|
|
165
|
+
@current_mutex.synchronize { @current = @current_hosts = nil }
|
|
166
|
+
end
|
|
121
167
|
|
|
122
168
|
# @param clock [#call, nil] monotonic seconds (specs)
|
|
123
|
-
|
|
169
|
+
# @param shared [Boolean] .current's own: refuses a client override
|
|
170
|
+
def initialize(hosts_config: nil, env: ENV, client_override: nil, clock: nil, shared: false)
|
|
124
171
|
@client_override = client_override
|
|
172
|
+
@shared = shared
|
|
125
173
|
@env = env
|
|
126
174
|
@clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
127
175
|
@adapters = {}
|
|
128
176
|
@host_lists = {}
|
|
129
|
-
raw = hosts_config ||
|
|
177
|
+
raw = hosts_config || HostConfig.all(env: env)
|
|
130
178
|
@entries = {}
|
|
131
179
|
raw.each do |key, cfg|
|
|
132
180
|
cfg = HostConfig.coerce(key, cfg)
|
|
@@ -155,45 +203,9 @@ module Samagotchi
|
|
|
155
203
|
|
|
156
204
|
attr_reader :entries
|
|
157
205
|
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
# Name suffixes that stay on a home or office network: mDNS, the
|
|
163
|
-
# special-use and customary private names, router defaults (fritz.box)
|
|
164
|
-
# and Tailscale's MagicDNS. Compared on whole labels.
|
|
165
|
-
LOCAL_NAME_SUFFIXES = %w[localhost local lan home home.arpa internal intranet localdomain private corp test
|
|
166
|
-
box ts.net].freeze
|
|
167
|
-
|
|
168
|
-
# Whether an address is remote: https, an http IP address outside the
|
|
169
|
-
# local nets (LOCAL_NETS), or an http name with a dot whose suffix isn't
|
|
170
|
-
# a local one (LOCAL_NAME_SUFFIXES): `gpu.example.com` is remote, while
|
|
171
|
-
# `box`, `mac.local`, `gpu.lan` and `pc.tail1234.ts.net` are local. chi
|
|
172
|
-
# doesn't look names up; hosts.<name>.remote overrides either way.
|
|
173
|
-
def self.remote_address?(scheme, host)
|
|
174
|
-
return true if scheme.to_s == "https"
|
|
175
|
-
|
|
176
|
-
address = IPAddr.new(host.to_s.delete_prefix("[").delete_suffix("]"))
|
|
177
|
-
address = address.native if address.ipv4_mapped?
|
|
178
|
-
LOCAL_NETS.none? { |net| net.family == address.family && net.include?(address) }
|
|
179
|
-
rescue IPAddr::Error
|
|
180
|
-
remote_name?(host)
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def self.remote_name?(host)
|
|
184
|
-
name = host.to_s.downcase.delete_suffix(".")
|
|
185
|
-
return false unless name.include?(".")
|
|
186
|
-
|
|
187
|
-
LOCAL_NAME_SUFFIXES.none? { |suffix| name == suffix || name.end_with?(".#{suffix}") }
|
|
188
|
-
end
|
|
189
|
-
private_class_method :remote_name?
|
|
190
|
-
|
|
191
|
-
# server.first_token_timeout, or nil when unset or unreadable.
|
|
192
|
-
def self.configured_first_token_timeout
|
|
193
|
-
Config.get("server.first_token_timeout")
|
|
194
|
-
rescue StandardError
|
|
195
|
-
nil
|
|
196
|
-
end
|
|
206
|
+
# The env its hosts, aliases and saved lists are read with (ENV, or chi
|
|
207
|
+
# self's): what an EffectiveModel's ModelLookup reads every layer with.
|
|
208
|
+
attr_reader :env
|
|
197
209
|
|
|
198
210
|
def default_entry
|
|
199
211
|
@entries["default"] || @entries.values.first
|