samagotchi 0.156.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 +44 -1
- data/bin/chi +1 -0
- data/docs/architecture.md +12 -7
- 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/bridge.md +83 -6
- data/docs/internals/bundles.md +28 -18
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +16 -2
- data/docs/internals/participants.md +3 -2
- data/docs/internals/remote-sessions.md +5 -2
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +1 -1
- data/docs/memory.md +13 -2
- data/docs/plugins.md +35 -2
- data/docs/sessions.md +9 -9
- data/docs/sub-agent.md +17 -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_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/bundle_command/check.rb +18 -0
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- 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/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/child_move.rb +6 -6
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +8 -22
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_sources.rb +5 -4
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine.rb +29 -3
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +6 -3
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_registry.rb +57 -45
- data/lib/samagotchi/kernel_loop.rb +7 -3
- data/lib/samagotchi/llm/chat_loop.rb +6 -3
- data/lib/samagotchi/llm_context_strategy.rb +27 -63
- 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_profile.rb +1 -0
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +41 -6
- data/lib/samagotchi/plugin/loader.rb +33 -10
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +27 -0
- 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/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +34 -35
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +321 -439
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +133 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/emptiness.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +29 -100
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_metrics.rb +7 -4
- 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/steer_cut_after.rb +7 -14
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- 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 +3 -1
- data/lib/samagotchi/testing/install.rb +2 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/tools/delegate.rb +48 -43
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- 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 +31 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- 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 +3 -3
- data/lib/samagotchi/web/session_control.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +64 -2
- data/lib/samagotchi/web/session_summary.rb +36 -24
- data/lib/samagotchi/worker.rb +28 -5
- data/lib/samagotchi/worker_link.rb +3 -0
- metadata +20 -4
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -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
|
|
@@ -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,13 +115,61 @@ 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 = {}
|
|
@@ -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
|
|
@@ -792,20 +792,24 @@ module Samagotchi
|
|
|
792
792
|
turn.qwen_attempts = 0
|
|
793
793
|
turn.qwen_partial = nil
|
|
794
794
|
runs = []
|
|
795
|
+
# The batch's ids before it runs (each run's events carry its own):
|
|
796
|
+
# nothing appends to the conversation during a batch, so they are the
|
|
797
|
+
# ids the entry would get after it (spec/tool_ids_pin_spec.rb).
|
|
798
|
+
ids = ToolIds.next_ids(turn.conversation, calls.size)
|
|
795
799
|
begin
|
|
796
800
|
ToolResponse.run_batch(tool_runner, calls, context: @context, iteration: turn.iteration, emit: turn.emit,
|
|
797
|
-
on_stream_event: turn.on_stream_event, cap: turn.cap) { |run, _| runs << run }
|
|
801
|
+
on_stream_event: turn.on_stream_event, cap: turn.cap, ids: ids) { |run, _| runs << run }
|
|
798
802
|
rescue Interrupt
|
|
799
803
|
# A Ctrl-C mid-batch: the entry still answers every call (the
|
|
800
804
|
# finished runs, then run_batch's interrupted ones), for #run to
|
|
801
805
|
# hand on with the conversation.
|
|
802
|
-
turn.conversation << ToolResponse.joined(runs, ids:
|
|
806
|
+
turn.conversation << ToolResponse.joined(runs, ids: ids.first(runs.size))
|
|
803
807
|
raise
|
|
804
808
|
end
|
|
805
809
|
turn.tool_activity.concat(ToolResponse.activities(runs))
|
|
806
810
|
# One entry for the batch: the capped outputs joined (ToolResponse.joined),
|
|
807
811
|
# an id per run (ToolIds).
|
|
808
|
-
turn.conversation << ToolResponse.joined(runs, ids:
|
|
812
|
+
turn.conversation << ToolResponse.joined(runs, ids: ids.first(runs.size))
|
|
809
813
|
turn.pending_tool_calls = true
|
|
810
814
|
:next
|
|
811
815
|
end
|
|
@@ -1132,10 +1132,13 @@ module Samagotchi
|
|
|
1132
1132
|
def dispatch(tool_calls, iteration, cap)
|
|
1133
1133
|
log_direct_deferred(tool_calls)
|
|
1134
1134
|
calls = tool_calls.map { |tool_call| NativeToolNormalizer.normalize(tool_call) }
|
|
1135
|
+
# The batch's ids before it runs (each run's events carry its own):
|
|
1136
|
+
# the same as one next id per result as it lands
|
|
1137
|
+
# (spec/tool_ids_pin_spec.rb).
|
|
1138
|
+
ids = ToolIds.next_ids(@conversation, calls.size)
|
|
1135
1139
|
runs = ToolResponse.run_batch(@loop.tool_runner, calls, context: @turn_context, iteration: iteration, emit: method(:emit),
|
|
1136
|
-
on_stream_event: @on_stream_event, cap: cap) do |run, index|
|
|
1137
|
-
@conversation << ToolResponse.single(run, tool_call_id: tool_calls[index].id,
|
|
1138
|
-
ids: ToolIds.next_ids(@conversation, 1))
|
|
1140
|
+
on_stream_event: @on_stream_event, cap: cap, ids: ids) do |run, index|
|
|
1141
|
+
@conversation << ToolResponse.single(run, tool_call_id: tool_calls[index].id, ids: [ids[index]])
|
|
1139
1142
|
end
|
|
1140
1143
|
@tool_activity.concat(ToolResponse.activities(runs))
|
|
1141
1144
|
end
|
|
@@ -148,10 +148,10 @@ module Samagotchi
|
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
# Where one value came from: :session, :model_setting, :host_setting
|
|
151
|
-
# or :config, and the place as
|
|
152
|
-
# "models: deepseek", "hosts
|
|
151
|
+
# or :config, and the place, named as the other settings name theirs
|
|
152
|
+
# ("session", "models: deepseek", "hosts.box", "llm_context.apply").
|
|
153
153
|
Origin = Data.define(:source, :where)
|
|
154
|
-
SESSION_ORIGIN = Origin.new(source: :session, where: "
|
|
154
|
+
SESSION_ORIGIN = Origin.new(source: :session, where: "session")
|
|
155
155
|
|
|
156
156
|
# What a turn's strategy, apply rule and budget are, each with its
|
|
157
157
|
# Origin (/llm-context, /stats, the web's info bar).
|
|
@@ -169,26 +169,22 @@ module Samagotchi
|
|
|
169
169
|
end
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
-
# The strategy for +
|
|
173
|
-
# @param
|
|
174
|
-
# @param names [Array<String>] HostRegistry#lookup_names
|
|
175
|
-
# @param models [Hash{String => ModelSettings}, nil] ModelSettings.all (specs)
|
|
172
|
+
# The strategy for +lookup+'s model's turn.
|
|
173
|
+
# @param lookup [ModelLookup] the model's layers (EffectiveModel#lookup)
|
|
176
174
|
# @param session [Array<Symbol>, nil] the session's own layers ([]: none)
|
|
177
175
|
# @param session_apply [Symbol, nil] the session's own apply rule
|
|
178
176
|
# @param session_budget [Integer, nil] the session's own budget (0: off)
|
|
179
177
|
# @return [Resolved]
|
|
180
|
-
def resolve(
|
|
181
|
-
explain(
|
|
182
|
-
session_budget: session_budget).resolved
|
|
178
|
+
def resolve(lookup, session: nil, session_apply: nil, session_budget: nil)
|
|
179
|
+
explain(lookup, session: session, session_apply: session_apply, session_budget: session_budget).resolved
|
|
183
180
|
end
|
|
184
181
|
|
|
185
182
|
# #resolve, with where each value came from.
|
|
186
183
|
# @return [Explained]
|
|
187
|
-
def explain(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
budget, budget_origin = budget_for(target, names, models, session_budget)
|
|
184
|
+
def explain(lookup, session: nil, session_apply: nil, session_budget: nil)
|
|
185
|
+
layers, source, where = layers_for(lookup, session)
|
|
186
|
+
apply, apply_origin = apply_for(lookup, session_apply)
|
|
187
|
+
budget, budget_origin = budget_for(lookup, session_budget)
|
|
192
188
|
resolved = resolved(layers, source, where).with(apply: apply, protect_steps: protect_steps,
|
|
193
189
|
stale_edits: Config.get(STALE_EDITS_SETTING) == true,
|
|
194
190
|
budget_tokens: budget)
|
|
@@ -196,79 +192,47 @@ module Samagotchi
|
|
|
196
192
|
budget_tokens: budget_origin)
|
|
197
193
|
end
|
|
198
194
|
|
|
199
|
-
# A session's LLM context strategy, apply rule and budget, each with
|
|
200
|
-
# where it came from (Explained#summary), as its worker would resolve
|
|
201
|
-
# them; nil when they can't be (an unreadable config, a model that
|
|
202
|
-
# resolves nowhere). Resolves from config and the registry only: no
|
|
203
|
-
# network, no model call. What a session page, a session list's ctx %
|
|
204
|
-
# and a spawn-time check all read.
|
|
205
|
-
# @param session [Session, nil]
|
|
206
|
-
# @param registry [HostRegistry, nil] the hosts to resolve against;
|
|
207
|
-
# HostRegistry.new by default (what the caller usually has)
|
|
208
|
-
# @return [Hash, nil] Explained#summary, symbol keys
|
|
209
|
-
def for_session(session, registry: nil)
|
|
210
|
-
return nil if session.nil?
|
|
211
|
-
|
|
212
|
-
# Here, not at the top: config.rb requires this file before ConfigFile
|
|
213
|
-
# exists, and effective_model.rb requires config.rb (via host_registry.rb).
|
|
214
|
-
require_relative "effective_model"
|
|
215
|
-
registry ||= HostRegistry.new
|
|
216
|
-
own = session.llm_context
|
|
217
|
-
EffectiveModel.of_session(session, registry: registry).llm_context_explained(own&.resolve_args || {})&.summary(own)
|
|
218
|
-
rescue StandardError
|
|
219
|
-
nil
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# The budget a session's turns run under (for_session's :budget_tokens),
|
|
223
|
-
# nil when it is off or can't be resolved. What a session list's ctx %
|
|
224
|
-
# counts against, so a saved session's fill reads as the live meter.
|
|
225
|
-
# @param session [Session, nil]
|
|
226
|
-
# @param registry [HostRegistry, nil] see .for_session
|
|
227
|
-
# @return [Integer, nil]
|
|
228
|
-
def session_budget(session, registry: nil)
|
|
229
|
-
for_session(session, registry: registry)&.fetch(:budget_tokens, nil)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
195
|
# [the budget, its Origin]: the session's own first (0 there is off,
|
|
233
196
|
# whatever the model says).
|
|
234
|
-
def budget_for(
|
|
197
|
+
def budget_for(lookup, session_budget)
|
|
235
198
|
return [session_budget.positive? ? session_budget : nil, SESSION_ORIGIN] unless session_budget.nil?
|
|
236
199
|
|
|
237
|
-
if (setting =
|
|
200
|
+
if (setting = lookup.model(:llm_context_budget_tokens))
|
|
238
201
|
return [setting.value, Origin.new(source: :model_setting, where: "models: #{setting.key}")]
|
|
239
202
|
end
|
|
240
203
|
|
|
241
|
-
budget =
|
|
242
|
-
return [budget, Origin.new(source: :host_setting, where: "hosts
|
|
204
|
+
budget = lookup.host(:llm_context_budget_tokens)
|
|
205
|
+
return [budget, Origin.new(source: :host_setting, where: "hosts.#{lookup.host_name}")] if budget
|
|
243
206
|
|
|
244
|
-
[parse_budget(
|
|
207
|
+
[parse_budget(lookup.global(BUDGET_SETTING).first, BUDGET_SETTING), Origin.new(source: :config, where: BUDGET_SETTING)]
|
|
245
208
|
end
|
|
246
209
|
|
|
247
|
-
def layers_for(
|
|
210
|
+
def layers_for(lookup, session)
|
|
248
211
|
return [session, :session, SESSION_ORIGIN.where] if session
|
|
249
212
|
|
|
250
|
-
if (setting =
|
|
213
|
+
if (setting = lookup.model(:llm_context_strategy))
|
|
251
214
|
return [setting.value, :model_setting, "models: #{setting.key}"]
|
|
252
215
|
end
|
|
253
216
|
|
|
254
|
-
layers =
|
|
255
|
-
return [layers, :host_setting, "hosts
|
|
217
|
+
layers = lookup.host(:llm_context_strategy)
|
|
218
|
+
return [layers, :host_setting, "hosts.#{lookup.host_name}"] if layers
|
|
256
219
|
|
|
257
|
-
[parse(
|
|
220
|
+
[parse(lookup.global(SETTING).first, SETTING) || [], :config, SETTING]
|
|
258
221
|
end
|
|
259
222
|
|
|
260
223
|
# [the apply rule, its Origin].
|
|
261
|
-
def apply_for(
|
|
224
|
+
def apply_for(lookup, session_apply)
|
|
262
225
|
return [session_apply, SESSION_ORIGIN] if session_apply
|
|
263
226
|
|
|
264
|
-
if (setting =
|
|
227
|
+
if (setting = lookup.model(:llm_context_apply))
|
|
265
228
|
return [setting.value, Origin.new(source: :model_setting, where: "models: #{setting.key}")]
|
|
266
229
|
end
|
|
267
230
|
|
|
268
|
-
apply =
|
|
269
|
-
return [apply, Origin.new(source: :host_setting, where: "hosts
|
|
231
|
+
apply = lookup.host(:llm_context_apply)
|
|
232
|
+
return [apply, Origin.new(source: :host_setting, where: "hosts.#{lookup.host_name}")] if apply
|
|
270
233
|
|
|
271
|
-
[parse_apply(
|
|
234
|
+
[parse_apply(lookup.global(APPLY_SETTING).first, APPLY_SETTING) || DEFAULT_APPLY,
|
|
235
|
+
Origin.new(source: :config, where: APPLY_SETTING)]
|
|
272
236
|
end
|
|
273
237
|
|
|
274
238
|
# llm_context.protect_steps: 0 or more (a negative one is the default).
|
|
@@ -75,6 +75,20 @@ module Samagotchi
|
|
|
75
75
|
plugin_file: plugin_file, script_files: script_files)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Whether the bundle file +src+ reads through a symlink: it is one, or
|
|
79
|
+
# it (or its dir, hooks/ say) resolves outside +bundle_dir+. A tar,
|
|
80
|
+
# zip or git repo keeps links; copying through one would install any
|
|
81
|
+
# file the user can read. MemoryFiles skips linked memories the same
|
|
82
|
+
# way. ShippedUpdate's plan reads it too.
|
|
83
|
+
def self.linked?(src, bundle_dir)
|
|
84
|
+
return true if File.symlink?(src)
|
|
85
|
+
|
|
86
|
+
root = File.realpath(bundle_dir)
|
|
87
|
+
!File.realpath(src).start_with?("#{root}/")
|
|
88
|
+
rescue SystemCallError
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
|
|
78
92
|
private
|
|
79
93
|
|
|
80
94
|
# Copies the bundle's hooks/*.rb into <bundle_dir>/hooks/: the ones
|
|
@@ -107,10 +121,8 @@ module Samagotchi
|
|
|
107
121
|
# One listed hook; an upgrade overwrites a local edit with a warning.
|
|
108
122
|
def install_hook(basename, src)
|
|
109
123
|
dest = File.join(@provenance.hooks_dir, basename)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return
|
|
113
|
-
end
|
|
124
|
+
result = result_for(src, dest, updated: @upgrade && @existing)
|
|
125
|
+
return @results[basename] = planned(result) if @dry_run
|
|
114
126
|
|
|
115
127
|
prev_sha = @existing&.hooks&.dig(basename)&.sha256
|
|
116
128
|
if @upgrade && File.exist?(dest) && !@force && !Provenance.recorded_sha(prev_sha).empty? && !Provenance.sha_matches?(dest, prev_sha)
|
|
@@ -118,16 +130,9 @@ module Samagotchi
|
|
|
118
130
|
end
|
|
119
131
|
|
|
120
132
|
FileUtils.mkdir_p(@provenance.hooks_dir)
|
|
121
|
-
unchanged = File.exist?(dest) && FileUtils.identical?(src, dest)
|
|
122
133
|
@trash.copy_edit(dest, as: "hooks/#{basename}", recorded: prev_sha, incoming: src)
|
|
123
134
|
place(src, dest)
|
|
124
|
-
@results[basename] =
|
|
125
|
-
{ status: "skipped", reason: "already up to date" }
|
|
126
|
-
elsif @upgrade && @existing
|
|
127
|
-
{ status: "updated" }
|
|
128
|
-
else
|
|
129
|
-
{ status: "installed" }
|
|
130
|
-
end
|
|
135
|
+
@results[basename] = result
|
|
131
136
|
@hook_files[basename] = dest
|
|
132
137
|
end
|
|
133
138
|
|
|
@@ -139,13 +144,13 @@ module Samagotchi
|
|
|
139
144
|
|
|
140
145
|
@bundle_hooks << basename
|
|
141
146
|
if @dry_run
|
|
142
|
-
@results[basename] =
|
|
147
|
+
@results[basename] = Installer::FileResult.new(word: :would_install)
|
|
143
148
|
else
|
|
144
149
|
FileUtils.mkdir_p(@provenance.hooks_dir)
|
|
145
150
|
dest = File.join(@provenance.hooks_dir, basename)
|
|
146
151
|
@trash.copy_edit(dest, as: "hooks/#{basename}", recorded: @existing&.hooks&.dig(basename)&.sha256, incoming: src)
|
|
147
152
|
place(src, dest)
|
|
148
|
-
@results[basename] =
|
|
153
|
+
@results[basename] = Installer::FileResult.new(word: :installed)
|
|
149
154
|
@hook_files[basename] = dest
|
|
150
155
|
end
|
|
151
156
|
end
|
|
@@ -166,22 +171,17 @@ module Samagotchi
|
|
|
166
171
|
end
|
|
167
172
|
end
|
|
168
173
|
|
|
169
|
-
# Strict mode:
|
|
170
|
-
#
|
|
174
|
+
# Strict mode: a hook whose sha256 differs from the one its manifest
|
|
175
|
+
# declares warns (as verify_memory_checksums does). The bundle's file is
|
|
176
|
+
# checked, the one the install copies, so a dry run checks what it
|
|
177
|
+
# would install, not the old hook in place.
|
|
171
178
|
def verify_hook_checksums
|
|
172
179
|
@bundle_hooks.each do |basename|
|
|
173
180
|
next if @conflicts.key?(basename)
|
|
174
181
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
dest = File.join(@provenance.hooks_dir, basename)
|
|
179
|
-
next unless File.exist?(dest)
|
|
180
|
-
|
|
181
|
-
actual = FileDigest.hex(dest)
|
|
182
|
-
if actual != expected
|
|
183
|
-
@warnings << "Checksum mismatch for hook #{basename}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
184
|
-
end
|
|
182
|
+
warning = FileDigest.mismatch("hook #{basename}", File.join(@bundle_dir, "hooks", basename),
|
|
183
|
+
@manifest.checksum_for_hook(basename))
|
|
184
|
+
@warnings << warning if warning
|
|
185
185
|
end
|
|
186
186
|
end
|
|
187
187
|
|
|
@@ -190,16 +190,13 @@ module Samagotchi
|
|
|
190
190
|
# installed are reported up to date, not "Installed".
|
|
191
191
|
# @return [Hash{String => String}] basename => installed path ({} on a dry run)
|
|
192
192
|
def install_guardrails(incoming_rules)
|
|
193
|
+
target = @provenance.guardrails_dir
|
|
194
|
+
results = incoming_rules.to_h { |src| [src, result_for(src, File.join(target, File.basename(src)), updated: false)] }
|
|
193
195
|
if @dry_run
|
|
194
|
-
|
|
196
|
+
results.each { |src, result| @results["guardrails/#{File.basename(src)}"] = planned(result) }
|
|
195
197
|
return {}
|
|
196
198
|
end
|
|
197
199
|
|
|
198
|
-
target = @provenance.guardrails_dir
|
|
199
|
-
unchanged = incoming_rules.select do |src|
|
|
200
|
-
dest = File.join(target, File.basename(src))
|
|
201
|
-
File.file?(dest) && FileUtils.identical?(src, dest)
|
|
202
|
-
end
|
|
203
200
|
@trash.copy_edits(target, "guardrails", @existing&.guardrails, incoming_rules.to_h { |src| [File.basename(src), src] })
|
|
204
201
|
FileUtils.rm_rf(target)
|
|
205
202
|
return {} if incoming_rules.empty?
|
|
@@ -208,8 +205,7 @@ module Samagotchi
|
|
|
208
205
|
incoming_rules.to_h do |src|
|
|
209
206
|
dest = File.join(target, File.basename(src))
|
|
210
207
|
place(src, dest)
|
|
211
|
-
@results["guardrails/#{File.basename(src)}"] =
|
|
212
|
-
unchanged.include?(src) ? { status: "skipped", reason: "already up to date" } : { status: "installed" }
|
|
208
|
+
@results["guardrails/#{File.basename(src)}"] = results[src]
|
|
213
209
|
[File.basename(src), dest]
|
|
214
210
|
end
|
|
215
211
|
end
|
|
@@ -235,29 +231,20 @@ module Samagotchi
|
|
|
235
231
|
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, @chi_version))
|
|
236
232
|
@warnings << "Plugin #{plugin[:file]} won't load: #{failure}"
|
|
237
233
|
end
|
|
234
|
+
dest = File.join(@provenance.plugin_dir, plugin[:file])
|
|
235
|
+
result = result_for(src, dest, updated: @upgrade && @provenance.installed?)
|
|
238
236
|
if @dry_run
|
|
239
|
-
@results[plugin[:file]] =
|
|
237
|
+
@results[plugin[:file]] = planned(result)
|
|
240
238
|
return nil
|
|
241
239
|
end
|
|
242
240
|
|
|
243
|
-
dest = File.join(@provenance.plugin_dir, plugin[:file])
|
|
244
|
-
unchanged = File.file?(dest) && FileUtils.identical?(src, dest)
|
|
245
241
|
@trash.copy_edits(@provenance.plugin_dir, "plugin", @existing&.plugin_shas, { plugin[:file] => src })
|
|
246
242
|
FileUtils.rm_rf(@provenance.plugin_dir)
|
|
247
243
|
FileUtils.mkdir_p(@provenance.plugin_dir)
|
|
248
244
|
place(src, dest)
|
|
249
|
-
@results[plugin[:file]] =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
{ status: "updated" }
|
|
253
|
-
else
|
|
254
|
-
{ status: "installed" }
|
|
255
|
-
end
|
|
256
|
-
expected = @manifest.checksum_for_plugin
|
|
257
|
-
actual = FileDigest.hex(dest)
|
|
258
|
-
if @strict && !@dev && expected && actual != expected
|
|
259
|
-
@warnings << "Checksum mismatch for plugin #{plugin[:file]}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
260
|
-
end
|
|
245
|
+
@results[plugin[:file]] = result
|
|
246
|
+
warning = FileDigest.mismatch("plugin #{plugin[:file]}", dest, @manifest.checksum_for_plugin) if @strict && !@dev
|
|
247
|
+
@warnings << warning if warning
|
|
261
248
|
dest
|
|
262
249
|
end
|
|
263
250
|
|
|
@@ -274,12 +261,15 @@ module Samagotchi
|
|
|
274
261
|
|
|
275
262
|
@warnings << "scripts/#{file} is not executable; chmod +x #{src}" if @dev && !File.executable?(src) && !page_only?(file)
|
|
276
263
|
end
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
264
|
+
results = scripts.keys.to_h do |file|
|
|
265
|
+
[file, result_for(File.join(@bundle_dir, "scripts", file), File.join(@provenance.scripts_dir, file),
|
|
266
|
+
updated: @upgrade && @provenance.installed?)]
|
|
267
|
+
end
|
|
268
|
+
if @dry_run
|
|
269
|
+
results.each { |file, result| @results["scripts/#{file}"] = planned(result) }
|
|
270
|
+
return {}
|
|
282
271
|
end
|
|
272
|
+
|
|
283
273
|
@trash.copy_edits(@provenance.scripts_dir, "scripts", @existing&.scripts,
|
|
284
274
|
scripts.keys.to_h { |file| [file, File.join(@bundle_dir, "scripts", file)] })
|
|
285
275
|
FileUtils.rm_rf(@provenance.scripts_dir)
|
|
@@ -289,20 +279,36 @@ module Samagotchi
|
|
|
289
279
|
scripts.to_h do |file, declared|
|
|
290
280
|
dest = File.join(@provenance.scripts_dir, file)
|
|
291
281
|
place(File.join(@bundle_dir, "scripts", file), dest)
|
|
292
|
-
@results["scripts/#{file}"] =
|
|
293
|
-
elsif @upgrade && @provenance.installed? then { status: "updated" }
|
|
294
|
-
else { status: "installed" }
|
|
295
|
-
end
|
|
282
|
+
@results["scripts/#{file}"] = results[file]
|
|
296
283
|
FileUtils.chmod(0o755, dest) unless @dev
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if @strict && !@dev && !expected.empty? && actual != expected
|
|
300
|
-
@warnings << "Checksum mismatch for script #{file}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
301
|
-
end
|
|
284
|
+
warning = FileDigest.mismatch("script #{file}", dest, declared.to_s.delete_prefix("sha256:")) if @strict && !@dev
|
|
285
|
+
@warnings << warning if warning
|
|
302
286
|
[file, dest]
|
|
303
287
|
end
|
|
304
288
|
end
|
|
305
289
|
|
|
290
|
+
# What the real run reports for +src+ over +dest+: up to date when the
|
|
291
|
+
# bytes are the same, else updated (+updated+: an upgrade over an
|
|
292
|
+
# install) or installed. A dry run records #planned of it, so the two
|
|
293
|
+
# can't drift.
|
|
294
|
+
def result_for(src, dest, updated:)
|
|
295
|
+
word = if File.file?(dest) && FileUtils.identical?(src, dest) then :up_to_date
|
|
296
|
+
elsif updated then :updated
|
|
297
|
+
else :installed
|
|
298
|
+
end
|
|
299
|
+
Installer::FileResult.new(word: word)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# A dry run's word for a result: would_install, would_update, or the
|
|
303
|
+
# up to date result as it is (nothing would be written).
|
|
304
|
+
def planned(result)
|
|
305
|
+
case result.word
|
|
306
|
+
when :installed then result.with(word: :would_install)
|
|
307
|
+
when :updated then result.with(word: :would_update)
|
|
308
|
+
else result
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
306
312
|
# A file only a block renderer's load names: the page loads it, nothing runs it.
|
|
307
313
|
def page_only?(file)
|
|
308
314
|
@manifest.block_renderers.any? { |renderer| renderer.load.include?(file) } &&
|
|
@@ -316,19 +322,8 @@ module Samagotchi
|
|
|
316
322
|
@dev ? File.symlink(File.realpath(src), dest) : FileUtils.cp(src, dest)
|
|
317
323
|
end
|
|
318
324
|
|
|
319
|
-
#
|
|
320
|
-
|
|
321
|
-
# zip or git repo keeps links; copying through one would install any
|
|
322
|
-
# file the user can read. MemoryFiles skips linked memories the same
|
|
323
|
-
# way.
|
|
324
|
-
def linked?(src)
|
|
325
|
-
return true if File.symlink?(src)
|
|
326
|
-
|
|
327
|
-
root = File.realpath(@bundle_dir)
|
|
328
|
-
!File.realpath(src).start_with?("#{root}/")
|
|
329
|
-
rescue SystemCallError
|
|
330
|
-
true
|
|
331
|
-
end
|
|
325
|
+
# AssetInstaller.linked? for this bundle's source.
|
|
326
|
+
def linked?(src) = self.class.linked?(src, @bundle_dir)
|
|
332
327
|
|
|
333
328
|
# linked? with a "<label> is a symlink (not installed)" warning.
|
|
334
329
|
def linked_out?(src, label)
|
|
@@ -227,7 +227,8 @@ module Samagotchi
|
|
|
227
227
|
files: files_map,
|
|
228
228
|
hooks: parts.hooks.empty? ? nil : parts.hooks,
|
|
229
229
|
trust_level: trust_level,
|
|
230
|
-
plugin: parts.plugin && { file: File.basename(parts.plugin), sha256: FileDigest.sha256(parts.plugin)
|
|
230
|
+
plugin: parts.plugin && { file: File.basename(parts.plugin), sha256: FileDigest.sha256(parts.plugin),
|
|
231
|
+
required: manifest&.plugin_required },
|
|
231
232
|
requires_chi: manifest&.requires_chi,
|
|
232
233
|
needs: manifest&.needs,
|
|
233
234
|
scripts: parts.scripts.transform_values { |src| FileDigest.sha256(src) },
|