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
|
@@ -35,11 +35,9 @@ module Samagotchi
|
|
|
35
35
|
LAYERS = %i[stale forget].freeze
|
|
36
36
|
# The layers a turn can run under so far.
|
|
37
37
|
BUILT = %i[stale forget].freeze
|
|
38
|
-
# The sentence forget_outputs' description carries (global only
|
|
39
|
-
#
|
|
38
|
+
# The sentence forget_outputs' description carries (global only; its
|
|
39
|
+
# default is Config::DEFAULT_LLM_CONTEXT_POLICY).
|
|
40
40
|
POLICY_SETTING = "llm_context.policy"
|
|
41
|
-
DEFAULT_POLICY = "Tidy at subtask boundaries: once a subtask is done, forget its tool outputs and note what it " \
|
|
42
|
-
"established; keep anything you'll still edit against."
|
|
43
41
|
|
|
44
42
|
APPLY_SETTING = "llm_context.apply"
|
|
45
43
|
APPLY_KEY = "llm_context_apply"
|
|
@@ -150,10 +148,10 @@ module Samagotchi
|
|
|
150
148
|
end
|
|
151
149
|
|
|
152
150
|
# Where one value came from: :session, :model_setting, :host_setting
|
|
153
|
-
# or :config, and the place as
|
|
154
|
-
# "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").
|
|
155
153
|
Origin = Data.define(:source, :where)
|
|
156
|
-
SESSION_ORIGIN = Origin.new(source: :session, where: "
|
|
154
|
+
SESSION_ORIGIN = Origin.new(source: :session, where: "session")
|
|
157
155
|
|
|
158
156
|
# What a turn's strategy, apply rule and budget are, each with its
|
|
159
157
|
# Origin (/llm-context, /stats, the web's info bar).
|
|
@@ -171,26 +169,22 @@ module Samagotchi
|
|
|
171
169
|
end
|
|
172
170
|
end
|
|
173
171
|
|
|
174
|
-
# The strategy for +
|
|
175
|
-
# @param
|
|
176
|
-
# @param names [Array<String>] HostRegistry#lookup_names
|
|
177
|
-
# @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
|
|
172
|
+
# The strategy for +lookup+'s model's turn.
|
|
173
|
+
# @param lookup [ModelLookup] the model's layers (EffectiveModel#lookup)
|
|
178
174
|
# @param session [Array<Symbol>, nil] the session's own layers ([]: none)
|
|
179
175
|
# @param session_apply [Symbol, nil] the session's own apply rule
|
|
180
176
|
# @param session_budget [Integer, nil] the session's own budget (0: off)
|
|
181
177
|
# @return [Resolved]
|
|
182
|
-
def resolve(
|
|
183
|
-
explain(
|
|
184
|
-
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
|
|
185
180
|
end
|
|
186
181
|
|
|
187
182
|
# #resolve, with where each value came from.
|
|
188
183
|
# @return [Explained]
|
|
189
|
-
def explain(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
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)
|
|
194
188
|
resolved = resolved(layers, source, where).with(apply: apply, protect_steps: protect_steps,
|
|
195
189
|
stale_edits: Config.get(STALE_EDITS_SETTING) == true,
|
|
196
190
|
budget_tokens: budget)
|
|
@@ -198,79 +192,47 @@ module Samagotchi
|
|
|
198
192
|
budget_tokens: budget_origin)
|
|
199
193
|
end
|
|
200
194
|
|
|
201
|
-
# A session's LLM context strategy, apply rule and budget, each with
|
|
202
|
-
# where it came from (Explained#summary), as its worker would resolve
|
|
203
|
-
# them; nil when they can't be (an unreadable config, a model that
|
|
204
|
-
# resolves nowhere). Resolves from config and the registry only: no
|
|
205
|
-
# network, no model call. What a session page, a session list's ctx %
|
|
206
|
-
# and a spawn-time check all read.
|
|
207
|
-
# @param session [Session, nil]
|
|
208
|
-
# @param registry [HostRegistry, nil] the hosts to resolve against;
|
|
209
|
-
# HostRegistry.new by default (what the caller usually has)
|
|
210
|
-
# @return [Hash, nil] Explained#summary, symbol keys
|
|
211
|
-
def for_session(session, registry: nil)
|
|
212
|
-
return nil if session.nil?
|
|
213
|
-
|
|
214
|
-
# Here, not at the top: config.rb requires this file before ConfigFile
|
|
215
|
-
# exists, and effective_model.rb requires config.rb (via host_registry.rb).
|
|
216
|
-
require_relative "effective_model"
|
|
217
|
-
registry ||= HostRegistry.new
|
|
218
|
-
own = session.llm_context
|
|
219
|
-
EffectiveModel.of_session(session, registry: registry).llm_context_explained(own&.resolve_args || {})&.summary(own)
|
|
220
|
-
rescue StandardError
|
|
221
|
-
nil
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
# The budget a session's turns run under (for_session's :budget_tokens),
|
|
225
|
-
# nil when it is off or can't be resolved. What a session list's ctx %
|
|
226
|
-
# counts against, so a saved session's fill reads as the live meter.
|
|
227
|
-
# @param session [Session, nil]
|
|
228
|
-
# @param registry [HostRegistry, nil] see .for_session
|
|
229
|
-
# @return [Integer, nil]
|
|
230
|
-
def session_budget(session, registry: nil)
|
|
231
|
-
for_session(session, registry: registry)&.fetch(:budget_tokens, nil)
|
|
232
|
-
end
|
|
233
|
-
|
|
234
195
|
# [the budget, its Origin]: the session's own first (0 there is off,
|
|
235
196
|
# whatever the model says).
|
|
236
|
-
def budget_for(
|
|
197
|
+
def budget_for(lookup, session_budget)
|
|
237
198
|
return [session_budget.positive? ? session_budget : nil, SESSION_ORIGIN] unless session_budget.nil?
|
|
238
199
|
|
|
239
|
-
if (setting =
|
|
200
|
+
if (setting = lookup.model(:llm_context_budget_tokens))
|
|
240
201
|
return [setting.value, Origin.new(source: :model_setting, where: "models: #{setting.key}")]
|
|
241
202
|
end
|
|
242
203
|
|
|
243
|
-
budget =
|
|
244
|
-
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
|
|
245
206
|
|
|
246
|
-
[parse_budget(
|
|
207
|
+
[parse_budget(lookup.global(BUDGET_SETTING).first, BUDGET_SETTING), Origin.new(source: :config, where: BUDGET_SETTING)]
|
|
247
208
|
end
|
|
248
209
|
|
|
249
|
-
def layers_for(
|
|
210
|
+
def layers_for(lookup, session)
|
|
250
211
|
return [session, :session, SESSION_ORIGIN.where] if session
|
|
251
212
|
|
|
252
|
-
if (setting =
|
|
213
|
+
if (setting = lookup.model(:llm_context_strategy))
|
|
253
214
|
return [setting.value, :model_setting, "models: #{setting.key}"]
|
|
254
215
|
end
|
|
255
216
|
|
|
256
|
-
layers =
|
|
257
|
-
return [layers, :host_setting, "hosts
|
|
217
|
+
layers = lookup.host(:llm_context_strategy)
|
|
218
|
+
return [layers, :host_setting, "hosts.#{lookup.host_name}"] if layers
|
|
258
219
|
|
|
259
|
-
[parse(
|
|
220
|
+
[parse(lookup.global(SETTING).first, SETTING) || [], :config, SETTING]
|
|
260
221
|
end
|
|
261
222
|
|
|
262
223
|
# [the apply rule, its Origin].
|
|
263
|
-
def apply_for(
|
|
224
|
+
def apply_for(lookup, session_apply)
|
|
264
225
|
return [session_apply, SESSION_ORIGIN] if session_apply
|
|
265
226
|
|
|
266
|
-
if (setting =
|
|
227
|
+
if (setting = lookup.model(:llm_context_apply))
|
|
267
228
|
return [setting.value, Origin.new(source: :model_setting, where: "models: #{setting.key}")]
|
|
268
229
|
end
|
|
269
230
|
|
|
270
|
-
apply =
|
|
271
|
-
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
|
|
272
233
|
|
|
273
|
-
[parse_apply(
|
|
234
|
+
[parse_apply(lookup.global(APPLY_SETTING).first, APPLY_SETTING) || DEFAULT_APPLY,
|
|
235
|
+
Origin.new(source: :config, where: APPLY_SETTING)]
|
|
274
236
|
end
|
|
275
237
|
|
|
276
238
|
# llm_context.protect_steps: 0 or more (a negative one is the default).
|
|
@@ -73,6 +73,15 @@ module Samagotchi
|
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# The view under +llm_context+'s strategy (LLMContextStrategy::Resolved,
|
|
77
|
+
# a turn's TurnSettings#llm_context), none without one: what both loops
|
|
78
|
+
# format the conversation through, so the warm-up warms the prompt the
|
|
79
|
+
# next turn sends.
|
|
80
|
+
# @return [LLMContextView]
|
|
81
|
+
def self.for(llm_context)
|
|
82
|
+
new(strategy: llm_context&.strategy || NONE)
|
|
83
|
+
end
|
|
84
|
+
|
|
76
85
|
# @param strategy [Symbol, Array<Symbol>] :none, or the layers
|
|
77
86
|
def initialize(strategy: NONE)
|
|
78
87
|
@layers = strategy == NONE ? [] : Array(strategy).map(&:to_sym) & LAYERS
|
|
@@ -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) },
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "file_state"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
module MemoryBundle
|
|
7
|
+
# What each command that acts on a bundle memory does in each FileState:
|
|
8
|
+
# one table per command, so a reader sees one command on one screen. A cell
|
|
9
|
+
# is keyed [kind, mark] or [kind, :any]; a dropped file another bundle
|
|
10
|
+
# lists too is [kind, :shared] (FileState#shared_with, asked only there).
|
|
11
|
+
# A dry run reads its command's row and doesn't execute it.
|
|
12
|
+
#
|
|
13
|
+
# A new kind or a new command adds a row in each table it reaches
|
|
14
|
+
# (docs/internals/bundles.md).
|
|
15
|
+
module FileActions
|
|
16
|
+
# do: what happens to the file's bytes
|
|
17
|
+
# :write_incoming the bundle's text replaces it (or is put there)
|
|
18
|
+
# :merge the text merge's result replaces it (Merger::Outcome)
|
|
19
|
+
# :decide not a function of kind and mark: the caller asks
|
|
20
|
+
# Merger::Upgrade#decision, and FileActions.decided
|
|
21
|
+
# turns its Outcome into the action
|
|
22
|
+
# :keep left as it is (already the bundle's, or kept)
|
|
23
|
+
# :skip left as it is, the bundle doesn't take it
|
|
24
|
+
# :move_to_trash moved to the trash (a removal)
|
|
25
|
+
# :refuse the command stops
|
|
26
|
+
# :restore the bundle's text put back (SystemBundle at its version)
|
|
27
|
+
# save_first: a copy of the old bytes goes to the trash before a write
|
|
28
|
+
# (when they differ from what is written)
|
|
29
|
+
# next_base: :on_disk (the file as the command leaves it), :incoming,
|
|
30
|
+
# :recorded (the base it has; none stays none), :none (no entry left)
|
|
31
|
+
# -- Merger::Outcome#base's vocabulary
|
|
32
|
+
# word: the report bucket the file is on (Installer#summary)
|
|
33
|
+
# record: the entry afterwards: :own (rebuilt, no mark), :keep_entry
|
|
34
|
+
# (kept with its base, no mark), :mark (marked behind), :hold_mark (as
|
|
35
|
+
# it was, mark and all), :clear_mark, :drop, :not_owned (never recorded)
|
|
36
|
+
Action = Data.define(:do, :save_first, :next_base, :word, :record) do
|
|
37
|
+
def initialize(do:, word:, next_base: :recorded, record: :own, save_first: false) = super
|
|
38
|
+
|
|
39
|
+
# The file stays behind the bundle, marked (a conflict to the report).
|
|
40
|
+
def behind? = record == :mark
|
|
41
|
+
|
|
42
|
+
# The bundle's text goes into the file: written (installed, forced or
|
|
43
|
+
# fast-forwarded) or merged into a local edit. The source file is the
|
|
44
|
+
# download, so the manifest's sha checks it (Installer's integrity
|
|
45
|
+
# check). :restore is SystemBundle's; it checks its own copy.
|
|
46
|
+
def takes_source? = %i[write_incoming merge].include?(self.do)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
WRITE = Action.new(do: :write_incoming, word: :installed, next_base: :on_disk)
|
|
50
|
+
UP_TO_DATE = Action.new(do: :keep, word: :up_to_date, next_base: :on_disk)
|
|
51
|
+
USER_SAME = Action.new(do: :keep, word: :up_to_date, next_base: :none, record: :not_owned)
|
|
52
|
+
USERS_OWN = Action.new(do: :skip, word: :kept_own, next_base: :none, record: :not_owned)
|
|
53
|
+
KEPT_OWN = Action.new(do: :skip, word: :kept_own, record: :keep_entry)
|
|
54
|
+
HELD = Action.new(do: :skip, word: :kept_own, record: :hold_mark)
|
|
55
|
+
FAST_FORWARD = Action.new(do: :write_incoming, word: :updated, next_base: :on_disk)
|
|
56
|
+
KEEP = Action.new(do: :keep, word: :kept)
|
|
57
|
+
# No base: no merge is possible, so the file stays behind, marked.
|
|
58
|
+
BEHIND = Action.new(do: :keep, word: :behind, record: :mark)
|
|
59
|
+
DECIDE = Action.new(do: :decide, word: nil)
|
|
60
|
+
REFUSE = Action.new(do: :refuse, word: nil, record: :hold_mark)
|
|
61
|
+
FORCED = Action.new(do: :write_incoming, word: :installed, next_base: :on_disk, save_first: true)
|
|
62
|
+
# The bundle dropped it: a plain install keeps it and its entry (the
|
|
63
|
+
# upgrade it points at moves it), an upgrade moves it unless edited.
|
|
64
|
+
DROPPED_KEPT = Action.new(do: :keep, word: nil, record: :keep_entry)
|
|
65
|
+
DROPPED_GONE = Action.new(do: :skip, word: nil, next_base: :none, record: :drop)
|
|
66
|
+
REMOVE = Action.new(do: :move_to_trash, word: :removed, next_base: :none, record: :drop)
|
|
67
|
+
KEPT_PRUNED = Action.new(do: :keep, word: :kept_pruned, record: :keep_entry)
|
|
68
|
+
KEPT_SHARED = Action.new(do: :keep, word: :kept_shared, next_base: :none, record: :drop)
|
|
69
|
+
|
|
70
|
+
# Rows every installing command shares: files the bundle doesn't own.
|
|
71
|
+
NOT_OWNED = {
|
|
72
|
+
%i[new any] => WRITE, %i[user_same any] => USER_SAME, %i[user_file any] => USERS_OWN,
|
|
73
|
+
%i[missing any] => WRITE, %i[level any] => UP_TO_DATE, %i[unreadable any] => REFUSE
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
# A plain install (a re-install over the record, or a fresh one).
|
|
77
|
+
INSTALL = NOT_OWNED.merge(
|
|
78
|
+
# A file never edited takes the source's text, as an upgrade's
|
|
79
|
+
# fast-forward does (D4); an edited one stays (an upgrade merges it).
|
|
80
|
+
%i[pristine none] => FAST_FORWARD, %i[pristine_no_base none] => FAST_FORWARD,
|
|
81
|
+
%i[edited none] => KEPT_OWN, %i[diverged none] => KEPT_OWN, %i[edited_no_base none] => KEPT_OWN,
|
|
82
|
+
# A marked file it skips has missed the bundle's change as much as before.
|
|
83
|
+
%i[pristine any] => HELD, %i[edited any] => HELD, %i[diverged any] => HELD,
|
|
84
|
+
%i[pristine_no_base any] => HELD, %i[edited_no_base any] => HELD,
|
|
85
|
+
%i[dropped_pristine any] => DROPPED_KEPT, %i[dropped_edited any] => DROPPED_KEPT,
|
|
86
|
+
%i[dropped_missing any] => DROPPED_GONE
|
|
87
|
+
).freeze
|
|
88
|
+
|
|
89
|
+
# install --force and upgrade --force: the bundle's text everywhere, an
|
|
90
|
+
# edit copied to the trash first. Its dropped rows are upgrade --force's
|
|
91
|
+
# (a plain install --force never prunes: its dropped files are INSTALL's).
|
|
92
|
+
FORCE = NOT_OWNED.merge(
|
|
93
|
+
# A file that already has the bundle's text is written all the same
|
|
94
|
+
# (nothing to save): it is installed, and checked, as any other.
|
|
95
|
+
%i[new any] => FORCED, %i[user_same any] => FORCED, %i[user_file any] => FORCED, %i[level any] => FORCED,
|
|
96
|
+
%i[pristine any] => FORCED, %i[edited any] => FORCED, %i[diverged any] => FORCED,
|
|
97
|
+
%i[pristine_no_base any] => FORCED, %i[edited_no_base any] => FORCED,
|
|
98
|
+
%i[dropped_pristine any] => REMOVE, %i[dropped_edited any] => REMOVE, %i[dropped_missing any] => DROPPED_GONE,
|
|
99
|
+
%i[dropped_pristine shared] => KEPT_SHARED, %i[dropped_edited shared] => KEPT_SHARED
|
|
100
|
+
).freeze
|
|
101
|
+
|
|
102
|
+
# An upgrade's 3-way view (Merger::Upgrade#outcome).
|
|
103
|
+
UPGRADE = NOT_OWNED.merge(
|
|
104
|
+
%i[pristine none] => FAST_FORWARD,
|
|
105
|
+
# A marked file that equals its base takes the update; a copy is kept.
|
|
106
|
+
%i[pristine current] => FAST_FORWARD.with(save_first: true),
|
|
107
|
+
# An older chi's mark: fast-forward unless it takes a line out and
|
|
108
|
+
# ShippedHistory can't prove the base (the legacy guard).
|
|
109
|
+
%i[pristine legacy] => DECIDE,
|
|
110
|
+
%i[edited any] => KEEP,
|
|
111
|
+
# The text merge: merged, a conflict, or already merged.
|
|
112
|
+
%i[diverged any] => DECIDE,
|
|
113
|
+
# No base: no merge. One never edited (the checksum vouches for it)
|
|
114
|
+
# takes the update as a file equal to its base would (U4); a marked
|
|
115
|
+
# one, or an edit, stays behind.
|
|
116
|
+
%i[pristine_no_base none] => FAST_FORWARD,
|
|
117
|
+
%i[pristine_no_base any] => BEHIND, %i[edited_no_base any] => BEHIND,
|
|
118
|
+
%i[dropped_pristine any] => REMOVE, %i[dropped_edited any] => KEPT_PRUNED, %i[dropped_missing any] => DROPPED_GONE,
|
|
119
|
+
%i[dropped_pristine shared] => KEPT_SHARED, %i[dropped_edited shared] => KEPT_SHARED
|
|
120
|
+
).freeze
|
|
121
|
+
|
|
122
|
+
# Remerge: the retry of a marked file at the same version. Unmarked
|
|
123
|
+
# files are never asked. Before the table, a mark whose source moved
|
|
124
|
+
# stays behind as it is (FileState#source_moved?).
|
|
125
|
+
REMERGE = {
|
|
126
|
+
%i[level any] => Action.new(do: :keep, word: :settled, next_base: :on_disk, record: :clear_mark),
|
|
127
|
+
%i[edited any] => Action.new(do: :keep, word: :settled, record: :clear_mark),
|
|
128
|
+
%i[diverged any] => DECIDE,
|
|
129
|
+
# It matches its old base: the next upgrade takes the update.
|
|
130
|
+
%i[pristine current] => BEHIND,
|
|
131
|
+
%i[pristine legacy] => DECIDE,
|
|
132
|
+
%i[pristine_no_base any] => BEHIND, %i[edited_no_base any] => BEHIND,
|
|
133
|
+
# Not shipped now: behind, nothing to merge.
|
|
134
|
+
%i[dropped_pristine any] => BEHIND, %i[dropped_edited any] => BEHIND,
|
|
135
|
+
%i[dropped_missing any] => Action.new(do: :skip, word: nil, record: :hold_mark),
|
|
136
|
+
# The next upgrade or a restore puts it back.
|
|
137
|
+
%i[missing any] => Action.new(do: :skip, word: nil, record: :hold_mark),
|
|
138
|
+
%i[unreadable any] => Action.new(do: :keep, word: :behind, record: :hold_mark)
|
|
139
|
+
}.freeze
|
|
140
|
+
|
|
141
|
+
# SystemBundle at its version: a memory not on disk is put back, one the
|
|
142
|
+
# record doesn't own too (research Q2, today's behaviour).
|
|
143
|
+
RESTORE = {
|
|
144
|
+
%i[missing any] => Action.new(do: :restore, word: :restored, next_base: :on_disk),
|
|
145
|
+
%i[new any] => Action.new(do: :restore, word: :restored, next_base: :on_disk)
|
|
146
|
+
}.freeze
|
|
147
|
+
|
|
148
|
+
# Uninstall, on the source-less kinds: an edit refuses (Uninstaller's
|
|
149
|
+
# --force moves it instead); a file another bundle lists stays.
|
|
150
|
+
UNINSTALL = {
|
|
151
|
+
%i[pristine any] => REMOVE, %i[pristine_no_base any] => REMOVE,
|
|
152
|
+
%i[edited any] => REFUSE, %i[edited_no_base any] => REFUSE, %i[unreadable any] => REFUSE,
|
|
153
|
+
%i[missing any] => DROPPED_GONE,
|
|
154
|
+
%i[pristine shared] => KEPT_SHARED, %i[pristine_no_base shared] => KEPT_SHARED,
|
|
155
|
+
%i[edited shared] => KEPT_SHARED, %i[edited_no_base shared] => KEPT_SHARED
|
|
156
|
+
}.freeze
|
|
157
|
+
|
|
158
|
+
TABLES = { install: INSTALL, force: FORCE, upgrade: UPGRADE, remerge: REMERGE, restore: RESTORE,
|
|
159
|
+
uninstall: UNINSTALL }.freeze
|
|
160
|
+
# The kinds whose row depends on another bundle listing the file.
|
|
161
|
+
SHARED_ASKED = %i[dropped_pristine dropped_edited pristine pristine_no_base edited edited_no_base].freeze
|
|
162
|
+
|
|
163
|
+
# The command's action for +state+: [kind, :shared] when another bundle
|
|
164
|
+
# lists a file that has such a row, else [kind, mark], then [kind, :any].
|
|
165
|
+
# @param command [Symbol] a TABLES key
|
|
166
|
+
# @return [Action, nil] nil: the command never meets the state
|
|
167
|
+
def self.for(command, state)
|
|
168
|
+
table = TABLES.fetch(command)
|
|
169
|
+
if SHARED_ASKED.include?(state.kind) && table.key?([state.kind, :shared]) && state.shared_with.any?
|
|
170
|
+
return table.fetch([state.kind, :shared])
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
table[[state.kind, state.mark]] || table[[state.kind, :any]]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# A :decide cell's answer: the action Merger::Upgrade#decision's Outcome
|
|
177
|
+
# stands for. Remerge never fast-forwards (the next upgrade does) and
|
|
178
|
+
# settles a file with nothing left to miss.
|
|
179
|
+
# @param outcome [Merger::Outcome]
|
|
180
|
+
# @param command [Symbol] :upgrade or :remerge
|
|
181
|
+
def self.decided(outcome, command = :upgrade)
|
|
182
|
+
case outcome.status
|
|
183
|
+
when :merged then Action.new(do: :merge, word: :merged, next_base: :incoming, save_first: true)
|
|
184
|
+
when :conflict then BEHIND
|
|
185
|
+
when :fast_forward then command == :remerge ? BEHIND : FAST_FORWARD.with(save_first: true)
|
|
186
|
+
when :keep, :noop
|
|
187
|
+
command == :remerge ? Action.new(do: :keep, word: :settled, next_base: outcome.base, record: :clear_mark) : KEEP.with(next_base: outcome.base)
|
|
188
|
+
else raise ArgumentError, "no action for outcome #{outcome.status.inspect}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -26,6 +26,19 @@ module Samagotchi
|
|
|
26
26
|
hex = hex(path)
|
|
27
27
|
hex && "sha256:#{hex}"
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
# The integrity check's one warning: +path+'s bytes against the sha
|
|
31
|
+
# hex a manifest declares for it. nil when they match, or when there is
|
|
32
|
+
# nothing to check (no sha declared, no file).
|
|
33
|
+
# @param label [String] what the file is ("a.md", "hook guard.rb")
|
|
34
|
+
def self.mismatch(label, path, expected)
|
|
35
|
+
return nil if expected.nil? || expected.empty?
|
|
36
|
+
|
|
37
|
+
actual = hex(path)
|
|
38
|
+
return nil if actual.nil? || actual == expected
|
|
39
|
+
|
|
40
|
+
"Checksum mismatch for #{label}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
41
|
+
end
|
|
29
42
|
end
|
|
30
43
|
end
|
|
31
44
|
end
|